アプリケーションのディレクトリ構成

Nette Framework のプロジェクトで、見通しがよく拡張しやすいディレクトリ構成をどう設計すればよいでしょうか。コードの整理に役立つ実績ある作法を紹介します。次のことを学びます。

  • アプリケーションをディレクトリに論理的に構造化する方法
  • プロジェクトの成長にうまく追随する構成の設計
  • 考えられる代替案とその長所・短所

まず大事なこととして、Nette Framework 自体は特定の構成を強制しません。どんな必要や好みにも簡単に合わせられるよう設計されています。

プロジェクトの基本構成

Nette Framework は決まったディレクトリ構成を押し付けませんが、Web Projectという形で実績のある既定の配置があります。

web-project/
├── app/              ← アプリケーションのディレクトリ
├── assets/           ← SCSS、JS ファイル、画像など。あるいは resources/
├── bin/              ← コマンドライン用のスクリプト
├── config/           ← 設定
├── log/              ← 記録されたエラー
├── temp/             ← 一時ファイル、キャッシュ
├── tests/            ← テスト
├── vendor/           ← Composer がインストールしたライブラリ
└── www/              ← 公開ディレクトリ(document-root)

この構成は必要に応じて自由に変えられます。フォルダの名前を変えたり移動したりできます。あとは Bootstrap.php と、必要なら composer.json のディレクトリへの相対パスを調整するだけです。それ以上は何も要りません。複雑な設定のやり直しも、定数の変更も不要です。Nette には賢い自動検出があり、アプリケーションの場所とその基準 URL を自動的に認識します。

コードを整理する原則

新しいプロジェクトをはじめて見るとき、素早く見当が付くべきです。app/Model/ ディレクトリをクリックして、次の構成が現れたと想像してください。

app/Model/
├── Services/
├── Repositories/
└── Entities/

ここから分かるのは、このプロジェクトが何らかのサービス、リポジトリ、エンティティを使っているということだけです。アプリケーションが実際に何をするのかは何も分かりません。

別のやり方、ドメインによる整理を見てみましょう。

app/Model/
├── Cart/
├── Payment/
├── Order/
└── Product/

こちらは違います。一目でネットショップだと分かります。ディレクトリの名前そのものが、アプリケーションに何ができるかを教えてくれます。支払い、注文、商品を扱うのだ、と。

ひとつめのやり方(クラスの種類による整理)は、実務でいくつかの問題を生みます。論理的に関係するコードが別々のフォルダに散らばり、そのあいだを行き来しなければなりません。ですからドメインで整理します。

名前空間

ディレクトリ構成をアプリケーションの名前空間に対応させるのが慣習です。つまりファイルの物理的な場所が、その名前空間と一致するということです。たとえば app/Model/Product/ProductRepository.php にあるクラスは、名前空間 App\Model\Product を持つべきです。この原則はコードを辿るのを助け、オートローディングを単純にします。

名前の単数形と複数形

アプリケーションの主要なディレクトリには単数形を使っていることに注目してください。appconfiglogtempwww です。アプリケーションの中でも同じで、ModelCorePresentation です。それぞれがひとつのまとまった概念を表しているからです。

同じく app/Model/Product は、商品に関わるすべてを表します。Products と呼ばないのは、商品が詰まったフォルダ(それなら nokia.phpsamsung.php のようなファイルが入るでしょう)ではないからです。商品を扱うクラス、ProductRepository.phpProductService.php を含む名前空間なのです。

app/Tasks フォルダが複数形なのは、独立した実行可能なスクリプトの集まり、CleanupTask.phpImportTask.php を含むからです。そのひとつひとつが独立した単位です。

一貫性のために、次をおすすめします。

  • 機能のまとまりを表す名前空間には単数形(複数のエンティティを扱う場合でも)
  • 独立した単位の集まりには複数形
  • 迷ったとき、あるいは考えたくないときは単数形

公開ディレクトリ www/

このディレクトリだけがウェブからアクセスできます(document-root です)。www/ の代わりに public/ という名前もよく見かけますが、これは慣習の問題にすぎず、アプリケーションの動作には影響しません。このディレクトリには次のものが入ります。

  • アプリケーションの入口 index.php
  • mod_rewrite の規則を含む .htaccess ファイル(Apache 用)
  • 静的ファイル(CSS、JavaScript、画像)
  • アップロードされたファイル

アプリケーションを適切に守るには、document-root が正しく設定されていることが決定的に重要です。

node_modules/ フォルダをこのディレクトリに置いては決していけません。実行可能かもしれない何千ものファイルが入っていて、公開すべきではありません。

アプリケーションのディレクトリ app/

アプリケーションのコードを含む主要なディレクトリです。基本の構成は次のとおりです。

app/
├── Core/               ← インフラに関わる事柄
├── Model/              ← ビジネスロジック
├── Presentation/       ← プレゼンターとテンプレート
├── Tasks/              ← コマンドのスクリプト
└── Bootstrap.php       ← アプリケーションの起動クラス

Bootstrap.phpアプリケーションの起動クラスで、環境を初期化し、設定を読み込み、DI コンテナを作ります。

では個々のサブディレクトリを詳しく見ていきましょう。

プレゼンターとテンプレート

アプリケーションのプレゼンテーション層は app/Presentation ディレクトリにあります。短い app/UI でもかまいません。ここがすべてのプレゼンター、そのテンプレート、そして関連する補助クラスの置き場です。

この層はドメインで整理します。ネットショップ、ブログ、API を組み合わせた複雑なプロジェクトなら、構成は次のようになります。

app/Presentation/
├── Shop/              ← ネットショップのフロント
│   ├── Product/
│   ├── Cart/
│   └── Order/
├── Blog/              ← ブログ
│   ├── Home/
│   └── Post/
├── Admin/             ← 管理画面
│   ├── Dashboard/
│   └── Products/
└── Api/               ← API のエンドポイント
	└── V1/

逆に単純なブログなら、次の構成を使います。

app/Presentation/
├── Front/             ← サイトのフロント
│   ├── Home/
│   └── Post/
├── Admin/             ← 管理画面
│   ├── Dashboard/
│   └── Posts/
├── Error/
└── Export/            ← RSS、サイトマップなど

Home/Dashboard/ のようなフォルダには、プレゼンターとテンプレートが入ります。Front/Admin/Api/ のようなフォルダはモジュールと呼ばれます。技術的には、アプリケーションを論理的に分けるために使うふつうのディレクトリです。

プレゼンターを含む各フォルダには、プレゼンターのファイル自体とそのテンプレートが入ります。たとえば Dashboard/ フォルダには次のものが入ります。

Dashboard/
├── DashboardPresenter.php     ← プレゼンター
└── default.latte              ← テンプレート

このディレクトリ構成はクラスの名前空間に反映されます。たとえば DashboardPresenterApp\Presentation\Admin\Dashboard 名前空間にあります(プレゼンターのマッピングをご覧ください)。

namespace App\Presentation\Admin\Dashboard;

class DashboardPresenter extends Nette\Application\UI\Presenter
{
	// ...
}

Admin モジュールの中の Dashboard プレゼンターは、アプリケーションの中でコロン記法を使って Admin:Dashboard と参照します。その default アクションは Admin:Dashboard:default です。入れ子のモジュールでは、コロンを複数使います。たとえば Shop:Order:Detail:default です。

構成の柔軟な発展

この構成の大きな利点のひとつが、プロジェクトの膨らむ要求に優雅に追随できることです。例として XML のフィードを生成する部分を取り上げましょう。最初は単純な形です。

Export/
├── ExportPresenter.php   ← すべてのエクスポート用のプレゼンター 1 つ
├── sitemap.latte         ← サイトマップのテンプレート
└── feed.latte            ← RSS フィードのテンプレート

やがてフィードの種類が増え、それらのためにもっとロジックが必要になります……。問題ありません。Export/ フォルダをそのままモジュールにするだけです。

Export/
├── Sitemap/
│   ├── SitemapPresenter.php
│   └── sitemap.latte
└── Feed/
	├── FeedPresenter.php
	├── amazon.latte         ← Amazon 用のフィード
	└── ebay.latte           ← eBay 用のフィード

この変身はまったく滑らかです。新しいサブフォルダを作り、コードをそこに分け、リンクを更新する(たとえば Export:feed から Export:Feed:amazon へ)だけです。おかげで必要に応じて構成を少しずつ広げられ、入れ子の深さにも制限はありません。

たとえば管理画面に、OrderDetailOrderEditOrderDispatch など注文管理に関わるプレゼンターがたくさんあるなら、整理のために Order という名前のモジュール(フォルダ)を作り、そこに DetailEditDispatch などのプレゼンター(のフォルダ)を入れられます。

テンプレートの置き場

これまでの例では、テンプレートがプレゼンターと同じフォルダに置かれていました。

Dashboard/
├── DashboardPresenter.php     ← プレゼンター
├── DashboardTemplate.php      ← 任意のテンプレートクラス
└── default.latte              ← テンプレート

実務ではこの置き場が最も便利だと分かっています。関係するファイルがすべてすぐ手元にあるからです。

あるいはテンプレートを templates/ のサブフォルダに置くこともできます。Nette はどちらの形にも対応しています。テンプレートを Presentation/ フォルダの完全に外に置くことさえできます。テンプレートの置き場の選択肢については、テンプレートの探索の章にすべてあります。

補助クラスとコンポーネント

プレゼンターとテンプレートには、ほかの補助ファイルが伴うことがよくあります。それらは適用範囲に応じて論理的に置きます。

1. そのプレゼンター固有のコンポーネントなら、プレゼンターと同じ場所に:

Product/
├── ProductPresenter.php
├── ProductGrid.php        ← 商品一覧のコンポーネント
└── FilterForm.php         ← 絞り込みのフォーム

2. モジュール用 – Accessory フォルダの利用をおすすめします。アルファベット順で都合よく先頭に来ます。

Front/
├── Accessory/
│   ├── NavbarControl.php    ← フロント用のコンポーネント
│   └── TemplateFilters.php
├── Product/
└── Cart/

3. アプリケーション全体用 – Presentation/Accessory/ に:

app/Presentation/
├── Accessory/
│   ├── LatteExtension.php
│   └── TemplateFilters.php
├── Front/
└── Admin/

あるいは LatteExtension.phpTemplateFilters.php のような補助クラスを、インフラのフォルダ app/Core/Latte/ に置くこともできます。コンポーネントは app/Components に。選び方はチームの慣習によります。

Model – アプリケーションの心臓

モデルには、アプリケーションのビジネスロジックがすべて入ります。その整理の規則もやはり、ドメインによる構造化です。

app/Model/
├── Payment/                   ← 支払いに関わるすべて
│   ├── PaymentFacade.php      ← 主な入口
│   ├── PaymentRepository.php
│   ├── Payment.php            ← エンティティ
├── Order/                     ← 注文に関わるすべて
│   ├── OrderFacade.php
│   ├── OrderRepository.php
│   ├── Order.php
└── Shipping/                  ← 配送に関わるすべて

モデルでは、ふつう次の種類のクラスに出会います。

ファサード: アプリケーションの中の特定のドメインへの主な入口を表します。まとめ役として働き、さまざまなサービスの協調を取り持って、「注文を作る」「支払いを処理する」といったユースケースをまるごと実現します。ファサードはその取りまとめの層の下に実装の詳細を隠し、アプリケーションのほかの部分に対して、そのドメインを扱うためのきれいなインターフェースを提供します。

class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// 検証
		// 注文の作成
		// メールの送信
		// 統計への書き込み
	}
}

サービス: ドメインの中の特定のビジネス上の操作に集中します。ユースケース全体を取りまとめるファサードと違い、サービスは具体的なビジネスロジック(価格の計算や支払いの処理など)を実装します。サービスはふつう状態を持たず、より複雑な操作の部品としてファサードから使われることも、もっと単純な用途でアプリケーションのほかの部分から直接使われることもあります。

class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// 価格の計算
	}
}

リポジトリ: データの保管場所、ふつうはデータベースとのやり取りをすべて担います。その役目はエンティティを読み書きし、それを探すメソッドを提供することです。リポジトリはアプリケーションのほかの部分をデータベースの実装の詳細から守り、データを扱うためのオブジェクト指向のインターフェースを提供します。

class OrderRepository
{
	public function find(int $id): ?Order
	{
	}

	public function findByCustomer(int $customerId): array
	{
	}
}

エンティティ: アプリケーションの主要なビジネス上の概念を表すオブジェクトで、自分の同一性を持ち、時とともに変化します。ふつうは ORM(Nette Database Explorer や Doctrine など)でデータベースのテーブルに対応づけられたクラスです。エンティティは、そのデータに関わるビジネスの規則や検証のロジックを持てます。

// 'orders' データベーステーブルに対応づけられたエンティティ
class Order extends Nette\Database\Table\ActiveRow
{
	public function addItem(Product $product, int $quantity): void
	{
		$this->related('order_items')->insert([
			'product_id' => $product->id,
			'quantity' => $quantity,
			'unit_price' => $product->price,
		]);
	}
}

値オブジェクト: 自分の同一性を持たない値、たとえば金額やメールアドレスを表す不変のオブジェクトです。同じ値を持つ 2 つの値オブジェクトのインスタンスは、同一とみなされます。

インフラのコード

Core/ フォルダ(あるいは Infrastructure/)は、アプリケーションの技術的な土台の置き場です。インフラのコードにはふつう次のものが含まれます。

app/Core/
├── Router/               ← ルーティングと URL の管理
│   └── RouterFactory.php
├── Security/             ← 認証と認可
│   ├── Authenticator.php
│   └── Authorizator.php
├── Logging/              ← ログ記録と監視
│   ├── SentryLogger.php
│   └── FileLogger.php
├── Cache/                ← キャッシュの層
│   └── FullPageCache.php
└── Integration/          ← 外部サービスとの統合
	├── Slack/
	└── Stripe/

小さなプロジェクトなら、当然ながら平らな構成で十分です。

Core/
├── RouterFactory.php
├── Authenticator.php
└── QueueMailer.php

ここに入るのは次のようなコードです。

  • 技術的なインフラ(ルーティング、ログ記録、キャッシュ)を扱う
  • 外部サービス(Sentry、Elasticsearch、Redis)と統合する
  • アプリケーション全体に基本的なサービス(メール、データベース)を提供する
  • 特定のドメインからはおおむね独立している。キャッシュやロガーは、ネットショップでもブログでも同じように働きます。

あるクラスがここに属するのか、モデルに属するのか迷いますか。決定的な違いは、Core/ のコードが次の性質を持つことです。

  • ドメイン(商品、注文、記事)について何も知らない
  • たいてい別のプロジェクトに持っていける
  • 「どう動くか」(メールをどう送るか)を解き、「何をするか」(どのメールを送るか)は解かない

分かりやすくするための例です。

  • App\Core\MailerFactory – メールを送るクラスのインスタンスを作り、SMTP の設定を扱います
  • App\Model\OrderMailer – MailerFactory を使って注文についてのメールを送り、そのテンプレートといつ送るべきかを知っています

コマンドのスクリプト

アプリケーションは、通常の HTTP リクエストの外で処理を行う必要にしばしば迫られます。バックグラウンドのデータ処理、メンテナンス、定期的な仕事などです。実行には bin/ ディレクトリの単純なスクリプトを使い、実装のロジック自体は app/Tasks/(あるいは app/Commands/)に置きます。

例を挙げます。

app/Tasks/
├── Maintenance/               ← メンテナンスのスクリプト
│   ├── CleanupCommand.php     ← 古いデータの削除
│   └── DbOptimizeCommand.php  ← データベースの最適化
├── Integration/               ← 外部システムとの統合
│   ├── ImportProducts.php     ← 仕入先システムからの取り込み
│   └── SyncOrders.php         ← 注文の同期
└── Scheduled/                 ← 定期的な仕事
	├── NewsletterCommand.php  ← ニュースレターの送信
	└── ReminderCommand.php    ← 顧客への通知

何がモデルに属し、何がコマンドのスクリプトに属するのでしょうか。たとえばメールを 1 通送るロジックはモデルの一部で、何千通ものメールの一括送信は Tasks/ に属します。

タスクはふつうコマンドラインか cron から実行します。bin/ のスクリプトが bootConsoleApplication()メソッドで DI コンテナを作り、そこから必要なサービスを取り出します。HTTP のリクエストから実行することもできますが、安全性を考える必要があります。タスクを実行するプレゼンターは、たとえばログイン済みのユーザーだけに限る、あるいは強力なトークンと許可された IP アドレスからのアクセスに限る、といった形で守らなければなりません。長時間かかるタスクでは、スクリプトの時間制限を延ばし、セッションのロックを避けるために session_write_close() を使う必要があります。

そのほかの考えられるディレクトリ

ここまで挙げた基本のディレクトリのほかにも、プロジェクトの必要に応じて専用のフォルダを足せます。よくあるものとその用途を見てみましょう。

app/
├── Api/              ← プレゼンテーション層から独立した API のロジック
├── Database/         ← マイグレーションのスクリプトとテストデータの seeder
├── Components/       ← アプリケーション全体で共有する画面の部品
├── Event/            ← イベント駆動のアーキテクチャを使うなら便利です
├── Mail/             ← メールのテンプレートと関連するロジック
└── Utils/            ← 補助クラス

アプリケーション全体のプレゼンターで使う共有の画面部品には、app/Componentsapp/Controls フォルダを使えます。

app/Components/
├── Form/                 ← 共有のフォームの部品
│   ├── SignInForm.php
│   └── UserForm.php
├── Grid/                 ← データ一覧の部品
│   └── DataGrid.php
└── Navigation/           ← ナビゲーションの要素
	├── Breadcrumbs.php
	└── Menu.php

ここには、より複雑なロジックを持つコンポーネントが属します。複数のプロジェクトでコンポーネントを共有したいなら、独立した Composer のパッケージに切り出すとよいでしょう。

app/Mail ディレクトリには、メールでのやり取りの管理を置けます。

app/Mail/
├── templates/            ← メールのテンプレート
│   ├── order-confirmation.latte
│   └── welcome.latte
└── OrderMailer.php

プレゼンターのマッピング

マッピングは、プレゼンター名からクラス名を導く規則を定めます。設定application › mapping キーで指定します。

このページでは、プレゼンターを app/Presentation フォルダ(または app/UI)に置いてきました。Nette Application 3.3 以降、これは設定しなくてよい既定の慣習です。別の構成を使う場合や、マッピングを明示的に指定したい場合、既定の設定は次の行に相当します。

application:
	mapping: App\Presentation\*\**Presenter

マッピングはどう働くのでしょうか。分かりやすくするために、まずモジュールのないアプリケーションを考えましょう。プレゼンターのクラスを App\Presentation 名前空間の下に置き、Home プレゼンターが App\Presentation\HomePresenter クラスに対応するようにしたいとします。それは次の設定で実現できます。

application:
	mapping: App\Presentation\*Presenter

マッピングは、マスク App\Presentation\*Presenter のアスタリスクをプレゼンター名 Home に置き換えて働き、最終的なクラス名 App\Presentation\HomePresenter になります。簡単ですね。

とはいえ、この章やほかの章の例で見たとおり、私たちはプレゼンターのクラスを同じ名前のサブディレクトリに置きます。たとえば Home プレゼンターは App\Presentation\Home\HomePresenter クラスに対応します。これは二重のアスタリスク ** で実現します(Nette Application 3.2.3 以上が必要です)。

application:
	mapping: App\Presentation\**Presenter

次はプレゼンターをモジュールに対応づける話に進みます。モジュールごとに個別のマッピングを定義できます。

application:
	mapping:
		Front: App\Presentation\Front\**Presenter
		Admin: App\Presentation\Admin\**Presenter
		Api: App\Api\*Presenter

この設定に従うと、プレゼンター Front:HomeApp\Presentation\Front\Home\HomePresenter クラスに、プレゼンター Api:OAuthApp\Api\OAuthPresenter クラスに対応します。

FrontAdmin のモジュールは似たマッピングの形をしていて、そうしたモジュールはこれからも増えそうなので、それらをまとめる一般的な規則を作れます。クラスのマスクにモジュール用の新しいアスタリスクを足します。

application:
	mapping:
		*: App\Presentation\*\**Presenter
		Api: App\Api\*Presenter

これはより深く入れ子になったディレクトリ構成でも働きます。たとえばプレゼンター Admin:User:Edit では、アスタリスクの部分がモジュールの階層ごとに繰り返され、クラス App\Presentation\Admin\User\Edit\EditPresenter になります。

別の書き方として、文字列の代わりに 3 つの要素から成る配列を使えます。上に示した例について、この書き方は前のものと等価です。

application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]
バージョン: 4.x