1. Entity, Value Object, DTO, Aggregate Root
Laravel có khái niệm là Model, dùng để tương tác với Database. Để đơn giản hoá việc lập trình, chúng ta sẽ đồng nhất Entity và Model làm một.
Các Model của Laravel cho phép Cast (Ép kiểu) khi đọc/ghi từ Database ra, mình xem đây như là Value Object.
Xem ví dụ sau cho dễ hiểu:
Quay lại bài thực hành số 3, ta đưa các Entity vào Laravel, như UserTransaction và User
<?php
use Illuminate\Database\Eloquent\Model;
enum UserTransactionPaygEnum: string
{
case TOPUP = 'topup';
case CONSUME = 'consume';
}
class UserTransaction extends Model
{
protected $table = 'user_transactions';
//fillable là tên các cột trong database
protected $fillable = [
'transaction_id',
'user_id',
'type',
'days',
];
protected function casts(): array
{
return [
'type' => UserTransactionPaygEnum::class
];
}
public static function make(
string $transactionId,
int $userId,
UserTransactionPaygEnum $type,
int $days
): static
{
return new static([
'transaction_id' => $transactionId,
'user_id' => $userId,
'type' => $type,
'days' => $days,
]);
}
public static function topup(
string $transactionId,
int $userId,
int $days
): static
{
return static::make(
$transactionId,
$userId,
UserTransactionPaygEnum::TOPUP,
$days
);
}
public static function consume(
string $transactionId,
int $userId,
int $days
): static
{
return static::make(
$transactionId,
$userId,
UserTransactionPaygEnum::CONSUME,
$days
);
}
}
//Entity kiêm root
class User extends Model
{
protected $table = 'users';
protected $fillable = [
'remaining_days'
];
public function exec(UserTransaction $userTransaction): void
{
$this->validateExec($userTransaction);
$this->remaining_days += match ($userTransaction->type) {
UserTransactionPaygEnum::TOPUP => $userTransaction->days,
UserTransactionPaygEnum::CONSUME => -$userTransaction->days,
};
}
private function validateExec(UserTransaction $userTransaction): void
{
if ($userTransaction->type == UserTransactionPaygEnum::CONSUME
&& $this->remaining_days < $userTransaction->days
) {
throw new NotEnoughDaysException();
}
}
}
2. Domain Service và Application Service
Nếu có chia folder Domain thì viết code Domain vào folder src/Domain, còn không để đơn giản thì tạo folder app/Services, viết các class Service của bài trước vào trong đó.
class DeployGameService
{
public function __construct(
public IUserRepository $userRepository,
public IUserTransactionRepository $userTransactionRepository,
public IGameScheduleRepository $gameScheduleRepository
)
{
}
public function deploy(
int $userId,
int $gameId,
LaunchGamePeriod $launchGamePeriod
): void
{
$user = $this->userRepository->findById($userId);
$gameSchedule = GameSchedule::make($gameId, $launchGamePeriod);
$transactionId = (string) \Illuminate\Support\Str::uuid();
$consumeTransaction = UserTransaction::consume(
$transactionId, $userId, $launchGamePeriod->durationInDays()
);
$user->exec($consumeTransaction);
//bỏ tất cả vào 1 transaction để đảm bảo tính toàn vẹn dữ liệu
\Illuminate\Support\Facades\DB::transaction(function () use ($gameSchedule, $user, $consumeTransaction) {
$this->gameScheduleRepository->saveActive($gameSchedule);
$this->userRepository->save($user);
$this->userTransactionRepository->save($consumeTransaction);
});
}
}
3. Interface Adapters
Ở tầng này, chủ yếu implement các hàm Repository để đọc ghi Entity. Chúng ta sử dụng OOP Design Pattern là Decorator để xử lý đọc ghi vào cache trước khi đọc ghi vào database, ví dụ mình chọn interface IUserRepository để implement. Code được viết ở folder app/Repositories
<?php
class UserRepository implements IUserRepository
{
public function save(User $user): void
{
$user->save();
}
public function findById(int $userId): ?User
{
return User::query()->find($userId);
}
}
//decorator UserRepository lại bằng cache
class UserCacheRepository implements IUserRepository
{
protected $cache;
public function __construct(
protected UserRepository $userRepository
)
{
$this->cache = \Illuminate\Support\Facades\Cache::driver('redis');
}
public function save(User $user): void
{
$this->userRepository->save($user);
$this->cache->forget($this->getCacheKey($user->id));
}
public function findById(int $userId): ?User
{
return $this->cache->remember(
$this->getCacheKey($userId),
60 * 15,
fn () => $this->userRepository->findById($userId)
);
}
protected function getCacheKey(int $userId): string
{
return 'user:'.$userId;
}
}
Sau đó mình thông báo cho Laravel về việc Adapter nào sẽ được dùng khi đấu nối vào tầng Application / Domain, bằng cách vào ServiceProvider của Laravel đăng ký:
<?php
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
IUserRepository::class,
UserCacheRepository::class
);
}
}
Có thể tạo class RepositoryServiceProvider để tiện việc quản lý Dependency.
4. Tầng Infrastructure
Đó chính là tất cả những gì thuộc về Laravel còn lại, chúng ta đặt toàn bộ code về Domain/Application lên bộ khung của Laravel đang sẵn có, với các file Entity, DomainService, ApplicationService, Adapter … đã code ở bài thực hành số 3.