0Pricing
PHP Academy · Lección

Casos de uso y servicios de aplicación

Exprese acciones de negocio como casos de uso independientes del framework.

Casos de uso y servicios de aplicación es una lección gratuita de PHP Academy en CoddyKit. Esta es la lección 3 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de PHP Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de PHP Academy incluye 4 lecciones en total.

Qué es realmente un caso de uso

Un caso de uso (también llamado servicio de aplicación o interactor) representa exactamente una operación específica de la aplicación: Registrar usuario, Realizar pedido, Cancelar suscripción. Coordina entidades y puertos para cumplir una única intención. Lo fundamental es que no depende de ningún framework: no contiene Request, ni Response, ni helpers globales; solo PHP convencional que puede invocarse desde cualquier lugar.

DTO de comando y DTO de resultado

Un caso de uso recibe como entrada un DTO de comando inmutable y devuelve un DTO de resultado. Los DTO son simples portadores de datos: no contienen comportamiento ni lógica de validación más allá de la estructura. Las propiedades readonly (PHP 8.1+) impiden que se alteren.

<?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) {}
}

El cuerpo del servicio de aplicación

El servicio traduce el comando en operaciones de dominio. Se encarga de la orquestación a nivel de aplicación —comprobar la unicidad, persistir y devolver identificadores—, mientras delega las reglas en las entidades.

<?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());
    }
}

Mantenga la lógica en las entidades

Tenga cuidado con el modelo de dominio anémico: entidades reducidas a getters/setters mientras toda la lógica se encuentra en los servicios. Las invariantes pertenecen a la entidad. El caso de uso debería leerse como un breve guion de intenciones, no como un muro de reglas de negocio.

<?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; }
}

Límites de transacción

Un caso de uso es el límite de transacción natural: un caso de uso equivale a una unidad de trabajo coherente. En lugar de llenar los servicios de llamadas a beginTransaction(), envuélvalos con un decorador transaccional para que el núcleo no dependa de la persistencia.

<?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));
    }
}

Validación: dónde corresponde

Divida la validación en dos niveles:

  • La validación de entrada (formato y campos obligatorios) se realiza en el adaptador de entrada o en un validador específico antes de ejecutar el caso de uso.
  • La validación de dominio (invariantes y reglas de negocio) reside en los objetos de valor y las entidades, que lanzan excepciones de dominio.

El caso de uso da por válida la estructura de entrada y hace cumplir su significado.

<?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;

Devolver resultados sin HTTP

Hay dos patrones para devolver datos sin dejar de ser independiente de los frameworks:

  • Devolver un DTO de resultado (sencillo y síncrono).
  • Puerto de salida / presentador: el caso de uso envía el resultado a un límite de salida inyectado y deja que el adaptador decida el formato (JSON, HTML o CLI). Así, incluso la forma de la respuesta queda fuera del núcleo.
<?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()));
    }
}

Eventos de dominio desde los casos de uso

Los casos de uso suelen registrar eventos de dominio que generan las entidades y luego distribuirlos después de confirmar la transacción. Esto desacopla los efectos secundarios (enviar el correo electrónico de bienvenida o actualizar el modelo de lectura) del flujo de trabajo del núcleo.

<?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;

Una clase por caso de uso

Prefiera una clase de una sola acción (un método público, a menudo __invoke) en lugar de un servicio enorme con diez métodos. Ventajas:

  • Responsabilidad única y nombres claros (CancelSubscription, no SubscriptionService::cancel).
  • El constructor inyecta solo lo que necesita esta operación.
  • Es fácil envolverla con decoradores (transacción, registro y autorización).

Configuración en la raíz de composición

El caso de uso nunca construye directamente sus dependencias; de eso se encarga la raíz de composición. Esta es una configuración manual que podría colocar en la definición de un contenedor DI.

<?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;

Aspectos transversales mediante decoradores

El registro, las métricas y la autorización son aspectos transversales; manténgalos fuera del cuerpo del caso de uso. Envuelva el servicio en decoradores que compartan su interfaz, de modo que el núcleo se concentre en el flujo de trabajo mientras los aspectos de infraestructura se componen a su alrededor.

<?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;
    }
}

Comprobación rápida

¿Dónde debería residir la regla «un correo electrónico debe ser único y tener un formato válido»?

Resumen

Los casos de uso independientes de frameworks proporcionan una capa de aplicación limpia:

  • Una clase de una sola acción por operación, que recibe un DTO de comando y devuelve un DTO de resultado (o envía el resultado a un puerto de salida).
  • Las entidades y los objetos de valor son dueños de las invariantes; el servicio solo orquesta. Evite los modelos anémicos.
  • Los casos de uso son límites de transacción, envueltos por decoradores en lugar de contener beginTransaction directamente.
  • Separe la validación de entrada (adaptador/VO) de la validación de dominio (entidades).
  • Los eventos de dominio desacoplan los efectos secundarios; la raíz de composición conecta las dependencias.

Preguntas frecuentes

¿La lección «Casos de uso y servicios de aplicación» es gratis?

Sí — el texto completo de «Casos de uso y servicios de aplicación» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de PHP Academy, actualiza a CoddyKit PRO. El curso de PHP Academy incluye 4 lecciones en total.

¿Qué aprenderé en «Casos de uso y servicios de aplicación»?

Exprese acciones de negocio como casos de uso independientes del framework. Practicas PHP Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar PHP Academy?

No se requiere experiencia previa. PHP Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 3 de 4.

¿Cuánto tiempo toma la lección «Casos de uso y servicios de aplicación»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de PHP Academy?

Sí. Cada lección de PHP Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. De la arquitectura por capas a la arquitectura limpia
  2. Puertos y adaptadores explicados
  3. Casos de uso y servicios de aplicación
  4. Inversión de dependencias en la práctica
← Volver a PHP Academy