SetupWizardの変更

初回セットアップウィザードを丸ごと再設計した。かなりボリュームが大きい作業で疲れた。

何が問題だったか

これまでのSetupWizardは、銀行口座の有無・現金の扱い・固定資産の有無・取引先の有無・期首残高……といった項目を、1つの巨大なウィザードで全部聞いて、その場で全部確定させる作りだった。項目が増えるたびにこのウィザードにStepを足していく形になっていて、初回登録の心理的な負荷がどんどん重くなっていた。

再設計の方針は「初回はほぼ判定だけに絞り、詳細入力は個別の導線に分ける」というもの。初回SetupWizardでは「銀行口座はありますか」「固定資産はありますか」のようなYes/No判定だけをInitialSetupDataに保存し、実際の登録作業(残高入力、科目登録など)はDashboardのカードから後で個別にやってもらう形にした。

ToDoモデルという土台

この再設計の前提として、まずTodoモデルを新しく作った。手入力のTodo・定期取引由来のTodo・システム検知由来のTodoを、将来的に同じ入れ物で扱うことを見据えた設計にした。

v1では発生源からの自動生成やDashboard統合はスコープ外にして、Console操作の最小APIだけに絞った。ここでも「全部を一度に作らない」を徹底している。

Todoの発生源はsource_type(manual / recurring / system)とpolymorphicなsource_model_*で表現する。発生源のモデルが消えてもTodoレコード自体は残す、というライフサイクルの独立性を最初から決めた。ここを曖昧にすると、後で「削除された定期取引のTodoがどう扱われるべきか」を都度考える羽目になる。

TodoHandlerという統一パターン

個別の登録作業(銀行口座登録、現金登録、固定資産登録、取引先登録、定期取引登録、期首仕訳登録……)は、種類ごとにフォームも検証ロジックも全然違う。これを1つの型で扱うためにTodoHandlerというインターフェースを作った。

interface TodoHandler
{
    public function todoType(): string;
    public function inputSchema(Todo $todo): array;
    public function validate(Todo $todo, array $inputs): array;
    public function execute(Todo $todo, array $validatedInputs, User $actor): void;
}

inputSchemaでフォームの形を宣言し、validateで検証し、executeで実際にドメインモデルへ書き込む。この形を決めてからは、新しい種類のToDoを追加するたびに同じ手順を繰り返すだけで済むようになった。実際、この期間で銀行口座・現金・取引先・定期取引・期首仕訳の5種類のHandlerを立て続けに追加している。パターンが決まっていると、機能追加のたびに一から設計判断をやり直さずに済むというのを実感した。

個別ウィザードは専用コンポーネントを作らなかった

設計の初期段階では、個別の登録画面ごとにSetup\BankAccountSetupWizardのような専用Livewireコンポーネントを立てる案を考えていた。だが実装を進める中で、その専用コンポーネント群は結局作らなかった。

個別SetupWizardの実体は、Dashboardに並ぶTodoと、それに紐づくTodoHandlerの組でしかない。画面描画はTodoCardという汎用コンポーネント(必要に応じてOpeningBalanceCardのような専用Cardに差し替え)が担い、todo_typeごとにTodo::$handlersでHandlerを引き当てる。「銀行口座用のウィザード画面」を新しく1枚作るのではなく、「wizard_bank_accountというtodo_typeにHandlerを1つ生やす」という粒度に落ち着いた。

// todo_type → Handler → Registration Service → 成果物
'wizard_bank_account' => BankAccountTodoHandler::class,   // → SubAccount(その他の預金)+期首仕訳
'wizard_cash_on_hand' => CashOnHandTodoHandler::class,    // → SubAccount(現金)+期首仕訳
'wizard_counterparty'  => CounterpartyTodoHandler::class, // → Counterparty

これはコンポーネント設計の変更というより、「初回の判定結果はInitialSetupDataの回答」「個別作業の進行状態はTodoのstatus」という2軸に分けたことの自然な帰結だった。専用コンポーネントを作る案は、Dashboardカードの見た目とTodoの状態遷移を1つのクラスに詰め込もうとして複雑になりかけていたが、TodoHandlerというインターフェースをすでに決めていたおかげで、「じゃあDashboard側もHandler経由で汎用的に描画すればいい」と気づけた。定期支出・定期収入のように画面がほぼ同じものは、共通のAbstractRecurringTransactionPlanTodoHandlerを継承したHandlerでまとめている。逆に家事按分のように「専用モデルを作るか迷ったが、既存の経費登録画面で扱う方が筋がいい」と気づいて新モデルを作らずに済ませたケースもある。

積み上がった個別ウィザード

この設計の上に、銀行口座・現金・固定費・取引先・期首仕訳の初期登録ウィザードとToDoカードを順番に積んでいった。