DDD中使用DTO时领域层如何定义响应结构(Laravel示例)
领域驱动设计(DDD)下Laravel认证模块的DTO与响应结构最佳实践
1. 响应结构的定义位置
响应结构(即DTO)必须定义在应用层,核心逻辑:
- 领域层仅负责封装核心业务规则与领域模型,不关心对外输出的格式或数据结构,只返回领域实体、值对象或领域服务的业务结果。
- 应用层作为领域层与外部的协调者,知晓用例的业务意图和对外暴露的数据需求,因此由它定义输入输出DTO是职责范围内的事。
- 表现层(如Controller)仅负责接收请求、调用应用层用例、将DTO转换为HTTP响应,不参与响应结构的定义。
2. DTO与领域层的同步方案
通过**映射器(Mapper)**实现DTO与领域模型的解耦,具体做法:
- 在应用层单独编写Mapper类,专门负责将领域实体/值对象转换为响应DTO。
- 当领域层修改实体字段时,仅需更新Mapper中的映射逻辑,无需修改DTO结构(除非业务需求要求响应字段变更)。
- 绝对避免在领域实体中添加
toDTO()这类方法,这会导致领域层依赖应用层,违反依赖倒置原则。
3. 领域层是否应处理令牌、格式化时间等内容?
绝对不应该,这些属于领域层之外的关注点:
- 令牌生成是基础设施层的职责(如Laravel Passport/JWT实现),属于技术细节,与核心业务规则无关。
- 多国时间格式化属于表现层/应用层的展示需求,领域层只处理与业务相关的时间逻辑(如用户创建时间、过期时间),不负责对外展示的格式化。
- 领域层应保持纯净,只关注“用户是否通过认证”这类核心业务问题,将技术细节和展示逻辑交给外层处理。
Laravel代码示例(清洁架构+DDD)
领域层:核心业务逻辑
// app/Domain/Entities/User.php namespace App\Domain\Entities; class User { public function __construct( public readonly string $id, public readonly string $name, public readonly string $email, private readonly string $passwordHash ) {} public function verifyPassword(string $plainPassword): bool { return password_verify($plainPassword, $this->passwordHash); } } // app/Domain/Services/AuthService.php namespace App\Domain\Services; use App\Domain\Entities\User; use App\Domain\Repositories\UserRepositoryInterface; class AuthService { public function __construct(private UserRepositoryInterface $userRepo) {} public function authenticate(string $email, string $password): ?User { $user = $this->userRepo->findByEmail($email); if (!$user || !$user->verifyPassword($password)) { return null; } return $user; } }
应用层:用例与DTO
// app/Application/Auth/DTOs/LoginResponseDTO.php namespace App\Application\Auth\DTOs; use Spatie\DataTransferObject\DataTransferObject; class LoginResponseDTO extends DataTransferObject { public string $user_id; public string $name; public string $email; public string $access_token; public array $localized_times; } // app/Application/Auth/Mappers/UserResponseMapper.php namespace App\Application\Auth\Mappers; use App\Domain\Entities\User; use App\Application\Auth\DTOs\LoginResponseDTO; class UserResponseMapper { public static function map(User $user, string $accessToken, array $localizedTimes): LoginResponseDTO { return new LoginResponseDTO([ 'user_id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'access_token' => $accessToken, 'localized_times' => $localizedTimes, ]); } } // app/Application/Auth/UseCases/LoginUseCase.php namespace App\Application\Auth\UseCases; use App\Domain\Services\AuthService; use App\Application\Auth\Mappers\UserResponseMapper; use App\Application\Auth\DTOs\LoginResponseDTO; use App\Infrastructure\Auth\TokenGeneratorInterface; use App\Infrastructure\Localization\TimeFormatterInterface; class LoginUseCase { public function __construct( private AuthService $authService, private TokenGeneratorInterface $tokenGenerator, private TimeFormatterInterface $timeFormatter ) {} public function execute(string $email, string $password): ?LoginResponseDTO { $user = $this->authService->authenticate($email, $password); if (!$user) return null; $accessToken = $this->tokenGenerator->generateForUser($user); $localizedTimes = $this->timeFormatter->formatCurrentTime(['en', 'zh']); return UserResponseMapper::map($user, $accessToken, $localizedTimes); } }
基础设施层:技术实现
// app/Infrastructure/Auth/PassportTokenGenerator.php namespace App\Infrastructure\Auth; use App\Domain\Entities\User; use Laravel\Passport\TokenRepository; use Laravel\Passport\ClientRepository; class PassportTokenGenerator implements TokenGeneratorInterface { public function __construct( private TokenRepository $tokenRepo, private ClientRepository $clientRepo ) {} public function generateForUser(User $user): string { $client = $this->clientRepo->find(1); $token = $this->tokenRepo->create( $user->id, $client->id, '', true ); return $token->accessToken; } } // app/Infrastructure/Localization/TimeFormatter.php namespace App\Infrastructure\Localization; use Carbon\Carbon; class TimeFormatter implements TimeFormatterInterface { public function formatCurrentTime(array $locales): array { $times = []; foreach ($locales as $locale) { Carbon::setLocale($locale); $times[$locale] = Carbon::now()->toDateTimeString(); } return $times; } }
表现层:HTTP控制器
// app/Http/Controllers/Auth/LoginController.php namespace App\Http\Controllers\Auth; use App\Application\Auth\UseCases\LoginUseCase; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; class LoginController extends Controller { public function __construct(private LoginUseCase $loginUseCase) {} public function __invoke(Request $request): JsonResponse { $request->validate([ 'email' => 'required|email', 'password' => 'required' ]); $responseDto = $this->loginUseCase->execute( $request->email, $request->password ); if (!$responseDto) { return response()->json(['message' => 'Invalid credentials'], 401); } return response()->json($responseDto->toArray()); } }
内容的提问来源于stack exchange,提问作者Mutaz Nayef
相关产品推荐
相关产品推荐

