はじめに
少しだけクリーンアーキテクチャやオニオンアーキテクチャについて感覚が掴めた気がするので記事にしておきます。
ご指摘点あれば遠慮なくお願いいたします。
構成ですがPHPとLaravelを使用、ディレクトリ構成はonion architectureで構築していきます。
実際の構成
以下ディレクトリ構成になります。
また色んなディレクトリ構成が存在するのでこれが正解ではないです。
呼び出し方とかおさえておけば色々対応できると思います。
├── app
│ ├── Http
│ │ ├──Controllers
│ │ │ ├── Api
├── onion
│ ├── Controller
│ ├── Driver
│ ├── Entity
│ └── UseCase
流れとしては
- routes/api.php
- Http/Controllers
- onion/Controllers
- onion/UseCase
- onion/Driver(Repository)
- onion/Entity
です。
UseCaseとDriverとEntityについて
UseCase
- ビジネスロジックやアプリケーションの具体的な操作を定義します
- 外部の技術的な詳細には依存せず、ビジネスルールに基づいた処理を実行します
- 必要なデータは、RepositoryやEntityを通じて取得・操作します
私なりの解釈ですが基本的にMVCのCの部分です。
このファイルを見れば何してるか分かるようになってるのが理想です。
Driver(Repository)
- 外部システム(データベースや外部API)と通信するためのインターフェースを提供します
- UseCaseから呼び出され、データの取得・保存を担当します
- 具体的な実装には依存せず、インターフェースを通じてデータ操作を行います
私なりの解釈ですが基本的にMVCSで言うところのSの部分です。
Model使ったり外部のAPI叩いたりとか
Entity
- ビジネスドメインの重要なオブジェクトやデータを表現します
- ビジネスルールやロジックを含み、アプリケーションのデータの一貫性を保ちます
- 必要に応じて操作ごとに分け、取得・更新・作成などの用途に応じたデータ構造を持ちます
私なりの解釈ですが基本的にMVCSで言うところのMの部分ですが結構違いがあってもっと柔軟にオブジェクトを構築します。
例えばユーザー情報取得と登録処理があったとします。
また使うテーブルはuser,role,hogeテーブルが必要とする場合
Entityは使用する各テーブルから必要なカラムを集めてオブジェクトを作る。
そのEntityを用いてDriver(Repository)でSQL書く
UseCaseはデータを取得し値を返すような流れです。
api.php
割愛します。
Http/Controllers
class HogeController extends Controller
{
/**
* @param \App\Http\Requests\Request $request
* @return \Illuminate\Http\Response
*/
public function hoge(Request $request)
{
$req_arr = $request->all();
return (new HogeHandler(new HogeUseCase(
new Hoge1EloquentRepository(),
new Hoge2EloquentRepository(),
// 他にRepository使うなら追加する。
new DatabaseConnection()
)))->handle($req_arr);
}
}
あくまでonionのcontorollerに使うusecaseとかrepositoryとか渡すだけです。
onion/Controllers
class HogeHandler
{
private $usecase;
public function __construct(HogeUseCaseInterface $usecase)
{
$this->usecase = $usecase;
}
public function handle(array $req_arr)
{
return $this->usecase->hoge($req_arr);
}
}
usecaseを呼んでます。
またusecaseやdriverはどんなメソッドがあるのか一覧で見やすくするためにInterfaceを挟んで実際のメソッドを呼びます。
onion/UseCase
// interface
interface HogeUseCaseInterface
{
public function __construct(
Hoge1RepositoryInterface $hoge1Repository,
Hoge2RepositoryInterface $hoge2Repository,
DatabaseConnectionInterface $db
);
public function hoge(array $req_arr): array;
}
// usecase
class HogeUseCase implements HogeUseCaseInterface
{
public function __construct(
Hoge1RepositoryInterface $hoge1Repository,
Hoge2RepositoryInterface $hoge2Repository,
DatabaseConnectionInterface $db
) {
$this->hoge1Repository = $hoge1Repository;
$this->hoge2Repository = $hoge2Repository;
$this->db = $db;
}
public function hoge(array $req_arr): array
{
$this->db->beginTransaction();
$response = [];
try {
// Entityを作成
$new_hoge = Hoge::create($req_arr);
// 作成したEntityを用いてDB登録する
$this->Hoge1Repository->create($new_hoge);
$this->db->commit();
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
return $response;
}
}
今回は登録処理をusecaseに書いております。
ただし、直接DBに登録するというよりはEntityを作成してそれを各DBに登録する流れになります。
onion/Entity
// HogeEntity作成
class Hoge
{
public HogeUser $hoge_user;
public HogeRole $hoge_role;
public HogeHoge $hoge_hoge;
public function __construct(array $properties = [])
{
$this->hoge_user = new HogeUser($properties);
$this->hoge_role = new HogeRole($properties);
$this->hoge_hoge = new HogeHoge($properties);
}
/**
* 販売契約の画面で入力された情報を元にHanbaiEntityを作成
*
* @param array $properties 更新するプロパティとその値を含む連想配列
*/
public static function create(array $properties): Hoge
{
// HogeEntityを作成
return new Hoge($properties);
}
HogeEntityはHogeUserとHogeRoleとHogeHogeというEntityを更に作成します
// HogeUserEntity作成
class HogeUser
{
public int $id;
public string $name;
public string $email;
public string $password;
public function __construct(array $properties = [])
{
foreach ($properties as $property => $value) {
if (property_exists($this, $property)) {
$this->$property = $value;
}
}
}
}
onion/Driver
// interface
interface Hoge1RepositoryInterface
{
public function create(Hoge $hoge): array;
}
// Repository
class HogeEloquentRepository implements HogeRepositoryInterface
{
// TODO 作成中
public function create(Hanbai $hanbai): array
{
try {
// 親テーブルhoge_userを作成
$eloquent_hoge_user = HogeUser::create(((array) $hoge->hoge_user));
// その他の子テーブルを作成
$eloquent_hoge_user->HogeRole()->create(array_merge((array) $hoge->hoge_role);
$eloquent_hoge_user->HogeHoge()->create(array_merge((array) $hoge->hoge_hoge);
} catch (Exception $e) {
throw new Exception($e->getMessage(), 0, $e);
}
}
Modelを使ってeloquentでDB登録していきます。
class HogeUser extends Model
{
// HogeUser は HogeRole を1つ持つ (hasOne)
public function hogeRole()
{
return $this->hasOne(HogeRole::class);
}
// HogeUser は HogeHoge を1つ持つ (hasOne)
public function hogeHoge()
{
return $this->hasOne(HogeHoge::class);
}
}
そしてusecaseに戻り何かレスポンスが必要であればセットしてあげて処理自体は終了になります。
// usecase
class HogeUseCase implements HogeUseCaseInterface
{
public function __construct(
Hoge1RepositoryInterface $hoge1Repository,
Hoge2RepositoryInterface $hoge2Repository,
DatabaseConnectionInterface $db
) {
$this->hoge1Repository = $hoge1Repository;
$this->hoge2Repository = $hoge2Repository;
$this->db = $db;
}
public function hoge(array $req_arr): array
{
$this->db->beginTransaction();
$response = [];
try {
// Entityを作成
$new_hoge = Hoge::create($req_arr);
// 作成したEntityを用いてDB登録する
$this->Hoge1Repository->create($new_hoge);
$this->db->commit();
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
return $response;
}
}
サンプルコードはPOST処理で、Entity作成してそのプロパティを持ってしてDBに登録する流れでした。
逆にGET処理ですと先にDBからデータを取ってきて返す値はEntityのプロパティに沿って返すという流れになります。
ついでなのでちょっとサンプルコード書いてみます。
// usecase
class HogeUseCase implements HogeUseCaseInterface
{
protected Hoge1RepositoryInterface $hoge1Repository;
protected Hoge2RepositoryInterface $hoge2Repository;
protected DatabaseConnectionInterface $db;
public function __construct(
Hoge1RepositoryInterface $hoge1Repository,
Hoge2RepositoryInterface $hoge2Repository,
DatabaseConnectionInterface $db
) {
$this->hoge1Repository = $hoge1Repository;
$this->hoge2Repository = $hoge2Repository;
$this->db = $db;
}
public function getHoge(int $id): array
{
try {
// Repositoryからデータを取得
$hogeData = $this->hoge1Repository->findById($id);
// 取得したデータをもとにエンティティを作成
$getHoge = new GetHoge($hogeData);
// エンティティを配列に変換して返す
return $getHoge->toArray();
} catch (Exception $e) {
// エラーハンドリング
throw $e;
}
}
}
// repository
class Hoge1Repository implements Hoge1RepositoryInterface
{
public function findById(int $id): array
{
// データベースからデータを取得(仮にEloquentを使用していると仮定)
$hoge = Hoge::find($id);
// 必要なデータのみを変換して返す
return [
'id' => $hoge->id,
'name' => $hoge->name,
'email' => $hoge->email,
// 'password' は含まない
];
}
}
// Entity
class GetHoge
{
public int $id;
public string $name;
public string $email;
// public string $password; // GET処理では含まない
public function __construct(array $properties = [])
{
foreach ($properties as $property => $value) {
if (property_exists($this, $property)) {
$this->$property = $value;
}
}
}
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}
GETでは不要なpasswordをEntityからあらかじめ外しておくことで
余計なフィールドが返却されないようになります。
よって効率的なデータ処理やセキリュティを高めることができます。
まとめ
簡単にオニオンアーキテクチャについての紹介でした。
以前勉強した際は全然理解できなかったのですが、少しだけぼんやりとつかめて気がします。
引き続き勉強してもっと使いこなせるように頑張ります。