openclassrooms/use-case
A lightweight PHP package for expressing business logic as explicit, single-responsibility use cases. Keeps controllers and models thin, makes domain code testable and framework-agnostic, and integrates cleanly with Laravel applications.
Install the package and inspect it before wiring anything:
composer require openclassrooms/use-case
Start with these files in vendor/openclassrooms/use-case:
README.md — the canonical usage examplesrc/UseCaseBus.php — how use cases are executed and how middleware is appliedsrc/UseCase.php / src/UseCaseRequest.php / src/UseCaseResponse.php — the contracts you implementsrc/Middleware/ — built-in cross-cutting concerns, if anycomposer.json — check for extra.laravel.providers; this package is likely framework-agnostic, so you will usually register it manuallyA minimal Laravel wiring looks like this:
// app/Providers/AppServiceProvider.php
use OpenClassrooms\UseCase\UseCaseBus;
public function register(): void
{
$this->app->singleton(UseCaseBus::class, function ($app) {
$bus = new UseCaseBus($app); // Laravel's container is PSR-11 compatible
// Adjust to the package's actual middleware API.
$bus->addMiddleware(new \App\UseCases\Middleware\TransactionMiddleware($app['db']));
$bus->addMiddleware(new \App\UseCases\Middleware\LoggingMiddleware($app['log']));
return $bus;
});
}
Your first use case should be a real business action, not CRUD for its own sake. For example, RegisterUser:
use OpenClassrooms\UseCase\UseCase;
final class RegisterUser implements UseCase
{
public function __construct(private UserRepository $users)
{
}
public function execute(RegisterUserRequest $request): RegisterUserResponse
{
$user = $this->users->create(
email: $request->email,
password: $request->password,
);
return new RegisterUserResponse($user->id);
}
}
Then call it from a controller after Laravel has validated the HTTP input:
public function store(RegisterUserHttpRequest $httpRequest, UseCaseBus $bus)
{
$response = $bus->execute(
RegisterUser::class, // or an instance, depending on the bus API
new RegisterUserRequest(
email: $httpRequest->validated('email'),
password: $httpRequest->validated('password'),
),
);
return new RegisterUserResource($response);
}
The first useful milestone is: HTTP request → Laravel validation → use case request DTO → bus → use case → response DTO → Laravel resource.
Keep controllers and jobs thin. The controller validates HTTP concerns, maps to a use case request, calls the bus, and maps the response back to HTTP. The use case owns the business transaction.
final class CreateInvoice implements UseCase
{
public function __construct(
private InvoiceRepository $invoices,
private CustomerRepository $customers,
) {
}
public function execute(CreateInvoiceRequest $request): CreateInvoiceResponse
{
$customer = $this->customers->findOrFail($request->customerId);
$invoice = $this->invoices->create(
customer: $customer,
lines: $request->lines,
);
return new CreateInvoiceResponse($invoice->id);
}
}
Use the bus as the integration point. Controllers, queued jobs, artisan commands, and event listeners should all execute the same use case through the bus. This keeps cross-cutting behavior consistent.
// app/Jobs/SendInvoiceEmail.php
public function handle(UseCaseBus $bus): void
{
$bus->execute(
SendInvoiceEmail::class,
new SendInvoiceEmailRequest($this->invoiceId),
);
}
Put cross-cutting concerns in bus middleware. Typical middleware:
final class TransactionMiddleware
{
public function __construct(private \Illuminate\Database\DatabaseManager $db)
{
}
public function handle(object $useCase, object $request, callable $next): object
{
return $this->db->transaction(fn () => $next($useCase, $request));
}
}
Keep transaction middleware close to the use case. Do not wrap the entire HTTP request unless you deliberately want validation, authorization, and response rendering inside the transaction.
Register use cases and dependencies in the container. If the bus resolves use cases by class name, Laravel's container must be able to construct them. Bind repository interfaces in a service provider:
$this->app->bind(UserRepository::class, EloquentUserRepository::class);
$this->app->bind(InvoiceRepository::class, EloquentInvoiceRepository::class);
If the bus accepts a use case instance instead, resolve it from the container at the call site or inject it into the controller.
Test at three levels.
public function test_it_registers_a_user(): void
{
$bus = $this->app->make(UseCaseBus::class);
$response = $bus->execute(
RegisterUser::class,
new RegisterUserRequest('[email protected]', 'secret'),
);
$this->assertNotNull($response->userId);
$this->assertDatabaseHas('users', ['email' => '[email protected]']);
}
Use request and response DTOs as boundaries. Do not return Eloquent models from use cases. Return a response DTO, then let a Laravel JsonResource or controller transform it for the transport layer.
final class RegisterUserResponse
{
public function __construct(public readonly int $userId)
{
}
}
This makes the use case reusable from HTTP, queues, commands, and tests without leaking persistence details.
There is probably no Laravel auto-discovery. Check composer.json for extra.laravel. If it is absent, you must register the bus and any middleware yourself. Do not assume a service provider exists.
Confirm the bus API before writing many use cases. The package is small, so read UseCaseBus.php first. Depending on the version, execute() may accept a use case instance, a class name, or a request object with an embedded use case. Standardize on one style in your codebase.
Watch the contract signatures. If the package's UseCase contract declares execute(UseCaseRequest $request): UseCaseResponse, your concrete request/response classes must implement those interfaces. If the contract is a marker interface, you have more freedom. Check the source before designing DTOs.
Do not nest transactions accidentally. If Laravel's DB::transaction() is already active in a controller or job, the transaction middleware should either join the existing transaction or be skipped. Nested transactions can hide failures and make rollback behavior confusing.
Middleware order matters. A typical order is:
Authorization should generally happen before opening a transaction. Event dispatch should happen after the business operation succeeds.
Be careful with Laravel container resolution. If the bus resolves use case class names, every constructor dependency must be container-resolvable. Bind interfaces, avoid scalar constructor arguments unless you provide contextual bindings, and test resolution with app()->make(YourUseCase::class).
Do not let the pattern become ceremony. Use cases are most valuable for actions with business rules, multiple side effects, or reuse across HTTP and queues. Simple CRUD endpoints can stay as conventional Laravel controllers. Forcing every index and show action through a use case adds indirection without much payoff.
Debug with Laravel's existing tools. Log the use case class, a request correlation ID, and the response type from middleware. Telescope can show the surrounding HTTP request, but it will not automatically understand the bus pipeline. Add explicit bus-level logging if you need to trace failures.
Extension points are likely custom middleware and decorators. If the package does not provide a needed concern, implement it as bus middleware rather than adding it to every use case. If the bus itself is too limited, decorate it behind your own application-level interface so you can swap implementations later.
Treat the package as low-support infrastructure. With zero stars and zero dependents, community examples will be scarce. Pin the version in composer.lock, read the source, and keep your integration isolated behind a small application service provider. That way, replacing the package later is a contained change rather than a rewrite.
How can I help you explore Laravel packages today?