openclassrooms/use-case-bundle
A Symfony bundle for organizing application logic into explicit use cases. It provides base classes, interfaces, and service wiring to define and execute use cases cleanly, keeping controllers thin and business rules isolated and testable.
First, the framing: this is a Symfony bundle, not a Laravel package. It depends on symfony/dependency-injection and symfony/http-kernel, so it will not install cleanly into a Laravel app. You have two realistic paths:
openclassrooms/use-case) and wire it yourself, or lift the pattern. Treat the bundle as a reference implementation, not a dependency.Version check before you install. The current release is v5.0.0, which is a major bump whose only stated change is "Bump for php8.5" (PR #25). It contains no new features and no new API — it is a toolchain/platform-compatibility release. Practically that means the composer.json platform constraints (PHP and Symfony ranges) moved, and anything older was dropped. Read the constraint block before you commit to a version:
composer show openclassrooms/use-case-bundle --all | head -40
If your app is not on the PHP/Symfony combination v5 targets, stay on v4.x — the architecture and tags described below are unchanged.
composer require openclassrooms/use-case-bundle
Register it in config/bundles.php:
return [
// ...
OpenClassrooms\UseCaseBundle\OpenClassroomsUseCaseBundle::class => ['all' => true],
];
Where to look first, in order:
DependencyInjection/Configuration.php and …Extension.php — the config tree tells you every knob the bundle exposes (security, transaction, event dispatching, tag names). This is the real contract.DependencyInjection/CompilerPass/ — that's where use cases get discovered from tags and where the bus routing table is built. If dispatch doesn't work, the compiler pass is why.First use case — the triad:
// 1. Request DTO — this is the dispatch key
final class CreateUserRequest implements UseCaseRequest
{
public bool $isAdmin = false;
public function __construct(public string $email) {}
}
// 2. The use case itself
final class CreateUser implements UseCase
{
public function __construct(private UserRepository $users) {}
public function execute(UseCaseRequest $request): void
{
$this->users->save(new User($request->email));
}
}
// 3. Dispatch from a controller/service
final class UserController
{
public function __construct(private UseCaseBus $bus) {}
public function __invoke(CreateUserRequest $request): Response
{
$this->bus->execute($request);
return new Response(status: 201);
}
}
Tag the service (or rely on autoconfiguration if the bundle declares it):
App\UseCase\CreateUser:
tags:
- { name: openclassrooms.use_case, request: App\UseCase\CreateUserRequest }
Your first meaningful use case is almost always the write path (create/update) with a transaction middleware, because that's where the bus earns its keep.
Don't fight it. Bind the core UseCaseBus in a service provider, keep a request => useCase map in config/use_cases.php, and dispatch through Laravel's container. You lose the compiler pass and gain explicitness. See the porting pattern below. Note that the v5 bump doesn't change this calculus at all — the http-kernel/dependency-injection coupling is still there.
The bus keys off the request's FQCN. Registration is therefore a map, not a routing table you debug by hand. The everyday rhythm is:
CreateUserRequest, ArchiveProjectRequest).execute(UseCaseRequest $request).request: pointing at the DTO.UseCaseBus where you need to fire it.Keep DTOs dumb (public/readonly properties, no logic). Keep use cases thin — orchestration only, no HTTP or Framework concerns.
The value of the bus is the pipeline: validation, authorization, transactions, logging. Configure the middleware stack once and every use case inherits it:
# config/packages/openclassrooms_use_case.yaml (verify keys against Configuration.php)
openclassrooms_use_case:
security:
enabled: true
transaction:
enabled: true
Add your own by implementing the provided UseCaseMiddleware contract and tagging it:
final class LoggingMiddleware implements UseCaseMiddleware
{
public function preExecute(UseCaseRequest $request): void
{
$this->logger->info('use_case.start', ['request' => $request::class]);
}
public function postExecute(UseCaseRequest $request): void
{
$this->logger->info('use_case.end', ['request' => $request::class]);
}
}
Ordering matters. Validation should run before authorization, authorization before the transaction opens. Check how the bundle composes the chain (usually by tag priority) and pin it explicitly.
Typical pattern: the middleware/use-case dispatches domain events after a successful execute. Use that to decouple side effects (emails, search indexing) from the write path. Keep event listeners outside the transaction where possible — otherwise a slow listener rolls back the whole use case.
execute($request) with a mocked repository. You don't need the bus, and you shouldn't test the pipeline here.// config/use_cases.php
return [
\App\UseCases\CreateUserRequest::class => \App\UseCases\CreateUser::class,
];
// UseCaseServiceProvider.php
public function register(): void
{
$this->app->bind(UseCaseBus::class, fn ($app) => new MapBus(
$app->make(config('use_cases'))
));
}
Wrap it in Laravel's Pipeline facade to get middleware parity:
return Pipeline::send($request)
->through([ValidateUseCase::class, AuthorizeUseCase::class, TransactionalUseCase::class])
->then(fn ($request) => $this->resolve($request)->execute($request));
v5.0.0 is a compatibility bump, not a feature release. The changelog is a single "Bump for php8.5" PR. That means: no new middleware, no new tag attributes, no behavioural changes promised. Treat the major-version number as a signal that constraints moved, not that the API moved. Upgrade deliberately — change your PHP runtime and the bundle in one step, and expect Composer resolver conflicts against older Symfony components rather than code errors.
Don't blind-pin ^5.0 if you're not on the target platform. A major bump for a runtime version means v5 likely requires the newest PHP line. On an older runtime, ^5.0 will simply not resolve, or will resolve with an older Symfony set you didn't intend. Pin explicitly ("^4.0" or "^5.0") and keep the platform requirements in composer.json in sync — this is exactly the kind of package where a floating constraint bites silently in CI.
The request FQCN is the routing key. Two use cases registered against the same request will silently overwrite each other. Never subclass or decorate a request class to "extend" a use case — create a distinct DTO. If dispatch behaves unpredictably, dump the compiled container (bin/console debug:container --tag=openclassrooms.use_case) before anything else.
Everything is compile-time. Tags, compiler passes, and the routing table are baked into the container cache. After changing the tag, config, or Configuration.php, run bin/console cache:clear (and in dev, debug:container to confirm). "I added the use case and the bus still says unknown request" is almost always a stale cache or a missed tag.
No validation unless you ask for it. The bus will happily hand a half-populated DTO to your use case. Either put validation in the request's constructor (assertions / a validated() factory), or enable the validation middleware — but don't assume it's on by default.
Security middleware needs a real AuthorizationCheckerInterface. If symfony/security-bundle isn't installed or the checker isn't wired, the security branch of the configure step will fail or silently no-op depending on version. Decide explicitly whether you rely on it or authorize inside the use case.
The tag attribute names are version-sensitive. The bundle has low adoption and a small surface — the README's exact attribute spelling (request, security, event, transaction…) is what the compiler pass reads. If a tag looks right but the compiler pass ignores it, open DependencyInjection/CompilerPass and read the getDefinition/findTaggedServiceIds calls; that's ground truth over docs.
Maturity caution — unchanged by v5. Still a small package: negligible star count, essentially no dependents, and a release cadence driven by internal needs. The v5 release reinforces the pattern: it's a maintenance bump to keep the package installable, not a feature roadmap. This reads as an internal architecture pattern published as a package. Pin the version, and if you depend on it heavily, vendor-fork or copy the middleware contracts into your app so a future platform bump doesn't block you.
Don't try to install it in Laravel. Composer will resolve but the http-kernel/dependency-injection coupling and Symfony-specific bundle bootstrap won't fit the Laravel lifecycle. Use the core library or reimplement the pattern — the concepts (request DTO, use case, bus, middleware) cost a few hundred lines to copy and you own them. The v5 PHP bump does nothing to change this.
Extension points worth knowing:
execute() so use cases stay transactionally clean.Debugging checklist when a use case "isn't found":
bin/console cache:clearbin/console debug:container --tag=openclassrooms.use_case — is your service listed?request: attribute the exact FQCN (including App\ namespace, case-sensitive)?composer show openclassrooms/use-case-bundle to confirm you actually got the major you asked for — a resolver conflict can quietly keep you on v4.How can I help you explore Laravel packages today?