Use Cases and Application Services
Express business actions as framework-free use cases.
Use Cases and Application Services is a free PHP Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What a Use Case Really Is
A use case (a.k.a. application service or interactor) captures exactly one application-specific operation: Register User, Place Order, Cancel Subscription. It coordinates entities and ports to fulfill a single intent. Crucially, it is framework-free: no Request, no Response, no global helpers — just plain PHP you can call from anywhere.
Command and Result DTOs
A use case takes an immutable command DTO as input and returns a result DTO. DTOs are dumb data carriers — no behavior, no validation logic beyond shape. Readonly properties (PHP 8.1+) make them tamper-proof.
<?php
final class RegisterUserCommand
{
public function __construct(
public readonly string $email,
public readonly string $plainPassword,
) {}
}
final class RegisterUserResult
{
public function __construct(public readonly string $userId) {}
}The Application Service Body
The service translates the command into domain operations. It does the application-level orchestration — uniqueness checks, persistence, returning identifiers — while delegating rules to entities.
<?php
final class RegisterUser
{
public function __construct(
private Users $users,
private PasswordHasher $hasher,
) {}
public function __invoke(RegisterUserCommand $c): RegisterUserResult {
if ($this->users->existsByEmail($c->email)) {
throw new EmailAlreadyRegistered($c->email);
}
$user = User::register(
UserId::generate(),
new Email($c->email),
$this->hasher->hash($c->plainPassword),
);
$this->users->add($user);
return new RegisterUserResult((string) $user->id());
}
}Keep Logic in Entities
Beware the anemic domain model: entities reduced to getters/setters while all logic sits in services. Invariants belong on the entity. The use case should read like a short script of intentions, not a wall of business rules.
<?php
final class User
{
private function __construct(
private UserId $id,
private Email $email,
private string $passwordHash,
private bool $active = false,
) {}
public static function register(UserId $id, Email $e, string $hash): self {
return new self($id, $e, $hash); // invariants enforced here
}
public function activate(): void {
if ($this->active) throw new AlreadyActive();
$this->active = true;
}
public function id(): UserId { return $this->id; }
}Transaction Boundaries
A use case is the natural transaction boundary: one use case = one consistent unit of work. Rather than littering services with beginTransaction(), wrap them with a transactional decorator so the core stays persistence-agnostic.
<?php
interface TransactionManager {
public function transactional(callable $work): mixed;
}
final class TransactionalRegisterUser
{
public function __construct(
private RegisterUser $inner,
private TransactionManager $tx,
) {}
public function __invoke(RegisterUserCommand $c): RegisterUserResult {
return $this->tx->transactional(fn() => ($this->inner)($c));
}
}Validation: Where It Belongs
Split validation into two tiers:
- Input validation (format, required fields) happens in the driving adapter or a dedicated validator before the use case runs.
- Domain validation (invariants, business rules) lives in value objects and entities and throws domain exceptions.
The use case assumes well-formed input and enforces meaning.
<?php
final class Email
{
public function __construct(public readonly string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: $value");
}
}
}
try { new Email('nope'); } catch (Throwable $e) { echo $e->getMessage(), PHP_EOL; }
echo (new Email('a@b.com'))->value, PHP_EOL;Returning Output Without HTTP
Two patterns to return data while staying framework-free:
- Return a result DTO (simple, synchronous).
- Output port / presenter — the use case pushes the result to an injected output boundary, letting the adapter decide formatting (JSON, HTML, CLI). This keeps even the response shape outside the core.
<?php
interface RegisterUserOutput {
public function present(RegisterUserResult $r): void;
}
final class RegisterUserWithPresenter {
public function __construct(private Users $users, private PasswordHasher $h) {}
public function __invoke(RegisterUserCommand $c, RegisterUserOutput $out): void {
$user = User::register(UserId::generate(), new Email($c->email), $this->h->hash($c->plainPassword));
$this->users->add($user);
$out->present(new RegisterUserResult((string) $user->id()));
}
}Domain Events from Use Cases
Use cases often record domain events that entities raise, then dispatch them after the transaction commits. This decouples side effects (send welcome email, update read model) from the core workflow.
<?php
trait RecordsEvents {
private array $events = [];
protected function record(object $e): void { $this->events[] = $e; }
public function releaseEvents(): array {
$e = $this->events; $this->events = []; return $e;
}
}
final class UserRegistered {
public function __construct(public readonly string $userId) {}
}
// Use case calls $user->releaseEvents() and hands them to a dispatcher
echo 'event recorded pattern', PHP_EOL;One Class Per Use Case
Prefer a single-action class (one public method, often __invoke) over a fat service with ten methods. Benefits:
- Crisp single responsibility and naming (
CancelSubscription, notSubscriptionService::cancel). - Constructor injects only what this operation needs.
- Easy to wrap with decorators (transaction, logging, authorization).
Wiring at the Composition Root
The use case never news-up its dependencies; the composition root does. Here is a manual wiring you could place in a DI container definition.
<?php
$pdo = new PDO('sqlite::memory:');
$users = new PdoUsers($pdo);
$hasher = new BcryptHasher();
$register = new RegisterUser($users, $hasher);
// Decorate with a transaction boundary
$register = new TransactionalRegisterUser($register, new PdoTransactionManager($pdo));
// Driving adapter calls it
$result = $register(new RegisterUserCommand('dev@coddykit.com', 's3cret!'));
echo $result->userId, PHP_EOL;Cross-Cutting Concerns via Decorators
Logging, metrics, and authorization are cross-cutting — keep them out of the use-case body. Wrap the service in decorators that share its interface, so the core stays focused on the workflow while infrastructure concerns compose around it.
<?php
interface RegisterUserHandler {
public function __invoke(RegisterUserCommand $c): RegisterUserResult;
}
final class LoggingRegisterUser implements RegisterUserHandler {
public function __construct(
private RegisterUserHandler $inner,
private LoggerInterface $log,
) {}
public function __invoke(RegisterUserCommand $c): RegisterUserResult {
$this->log->info('register.start', ['email' => $c->email]);
$r = ($this->inner)($c);
$this->log->info('register.ok', ['id' => $r->userId]);
return $r;
}
}Quick Check
Where should the rule "an email must be unique and well-formed" live?
Recap
Framework-free use cases give you a clean application layer:
- One single-action class per operation, taking a command DTO, returning a result DTO (or pushing to an output port).
- Entities and value objects own invariants; the service only orchestrates — avoid anemic models.
- Use cases are transaction boundaries, wrapped by decorators rather than inline
beginTransaction. - Split input validation (adapter/VO) from domain validation (entities).
- Domain events decouple side effects; the composition root wires dependencies.
Frequently asked questions
Is the “Use Cases and Application Services” lesson free?
Yes — the full text of “Use Cases and Application Services” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.
What will I learn in “Use Cases and Application Services”?
Express business actions as framework-free use cases. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start PHP Academy?
No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Use Cases and Application Services” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this PHP Academy lesson?
Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- From Layered to Clean Architecture
- Ports and Adapters Explained
- Use Cases and Application Services
- Dependency Inversion in Practice