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

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/

ここでは違います – 一目でeコマースサイトであることがわかります。ディレクトリ名自体が、アプリケーションができること、つまり支払い、注文、製品を扱うことを示しています。

最初のアプローチ(クラスタイプによる整理)は、実際には多くの問題を引き起こします。論理的に関連するコードが異なるフォルダに分散され、それらの間を行き来する必要があります。したがって、ドメインごとに整理します。

名前空間

ディレクトリ構造がアプリケーションの名前空間に対応するのが慣例です。つまり、ファイルの物理的な場所がその名前空間に対応します。例えば、app/Model/Product/ProductRepository.php に配置されたクラスは、App\Model\Product 名前空間を持つべきです。この原則は、コードの理解を助け、オートローディングを簡素化します。

名前の単数形 vs 複数形

アプリケーションのメインディレクトリでは単数形を使用していることに注意してください:app, config, log, temp, www。同様に、アプリケーション内部でも:Model, Core, Presentation。これは、それぞれが1つのまとまった概念を表しているためです。

同様に、例えば app/Model/Product は製品に関するすべてを表します。Products とは呼びません。なぜなら、それは製品でいっぱいのフォルダではないからです(そこには nokia.php, samsung.php のようなファイルがあるでしょう)。それは、製品を扱うクラス、つまり ProductRepository.php, ProductService.php を含む名前空間です。

app/Tasks フォルダは複数形です。なぜなら、それは独立した実行可能なスクリプトのセット、つまり CleanupTask.php, ImportTask.php を含んでいるからです。それぞれが独立したユニットです。

一貫性のために、以下を使用することをお勧めします。

  • 機能的な全体を表す名前空間には単数形(複数のエンティティを扱う場合でも)
  • 独立したユニットのコレクションには複数形
  • 不確かな場合、またはそれについて考えたくない場合は、単数形を選択してください

公開ディレクトリ www/

このディレクトリは、Webからアクセスできる唯一のディレクトリ(いわゆるdocument-root)です。www/ の代わりに public/ という名前をよく見かけることもありますが、これは単なる慣例の問題であり、機能には影響しません。ディレクトリには以下が含まれます。

  • アプリケーションのエントリポイント index.php
  • mod_rewrite(Apacheの場合)のルールを含む .htaccess ファイル
  • 静的ファイル(CSS、JavaScript、画像)
  • アップロードされたファイル

アプリケーションの適切なセキュリティのためには、document-rootを正しく設定することが不可欠です。

このディレクトリに node_modules/ フォルダを決して配置しないでください。実行可能であり、公開すべきではない数千のファイルが含まれています。

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

これはアプリケーションコードを含むメインディレクトリです。基本構造:

app/
├── Core/               ← インフラストラクチャ関連
├── Model/              ← ビジネスロジック
├── Presentation/       ← Presenterとテンプレート
├── Tasks/              ← コマンドスクリプト
└── Bootstrap.php       ← アプリケーションのブートストラップクラス

Bootstrap.php は、環境を初期化し、設定をロードし、DIコンテナを作成するアプリケーションの起動クラスです。

次に、個々のサブディレクトリについて詳しく見ていきましょう。

Presenterとテンプレート

アプリケーションのプレゼンテーション部分は app/Presentation ディレクトリにあります。代替案は短い app/UI です。これは、すべてのPresenter、そのテンプレート、および可能なヘルパークラスのための場所です。

このレイヤーをドメインごとに整理します。eコマース、ブログ、APIを組み合わせた複雑なプロジェクトでは、構造は次のようになります。

app/Presentation/
├── Shop/              ← eコマースフロントエンド
│   ├── Product/
│   ├── Cart/
│   └── Order/
├── Blog/              ← ブログ
│   ├── Home/
│   └── Post/
├── Admin/             ← 管理画面
│   ├── Dashboard/
│   └── Products/
└── Api/               ← APIエンドポイント
	└── V1/

一方、単純なブログでは、次のような分割を使用します。

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

Home/Dashboard/ のようなフォルダには、Presenterとテンプレートが含まれます。Front/, Admin/, Api/ のようなフォルダはモジュールと呼ばれます。技術的には、これらはアプリケーションを論理的に分割するために使用される通常のディレクトリです。

Presenterを含む各フォルダには、同じ名前のPresenterとそのテンプレートが含まれます。例えば、Dashboard/ フォルダには以下が含まれます。

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

このディレクトリ構造は、クラスの名前空間に反映されます。例えば、DashboardPresenterApp\Presentation\Admin\Dashboard 名前空間に配置されます(mapování presenterůを参照)。

namespace App\Presentation\Admin\Dashboard;

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

Admin モジュール内の Dashboard Presenterには、アプリケーション内でコロン表記を使用して Admin:Dashboard として参照します。その default アクションには Admin:Dashboard:default として参照します。ネストされたモジュールの場合、複数のコロンを使用します。例えば Shop:Order:Detail:default です。

構造の柔軟な開発

この構造の大きな利点の1つは、プロジェクトの成長するニーズにエレガントに適応する方法です。例として、XMLフィードを生成する部分を取り上げましょう。最初は単純な形式です。

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

時間が経つにつれて、さらに多くのフィードタイプが追加され、それらに対してより多くのロジックが必要になります… 問題ありません!Export/ フォルダは簡単にモジュールになります。

Export/
├── Sitemap/
│   ├── SitemapPresenter.php
│   └── sitemap.latte
└── Feed/
	├── FeedPresenter.php
	├── zbozi.latte         ← Zboží.cz用フィード
	└── heureka.latte       ← Heureka.cz用フィード

この変換は完全にスムーズです – 新しいサブフォルダを作成し、コードをそれらに分割し、リンクを更新するだけです(例:Export:feed から Export:Feed:zbozi へ)。これにより、必要に応じて構造を徐々に拡張でき、ネストのレベルに制限はありません。

例えば、管理画面で注文管理に関連する多くのPresenter(OrderDetail, OrderEdit, OrderDispatch など)がある場合、より良い整理のために、この場所に Order モジュール(フォルダ)を作成できます。そこにはPresenter Detail, Edit, Dispatch などの(フォルダ)が含まれます。

テンプレートの配置

前の例では、テンプレートがPresenterと同じフォルダに直接配置されていることを見ました。

Dashboard/
├── DashboardPresenter.php     ← Presenter
├── DashboardTemplate.php      ← テンプレート用のオプションクラス
└── default.latte              ← テンプレート

この配置は、実際には最も便利であることが証明されています – すべての関連ファイルがすぐに手元にあります。

あるいは、テンプレートを templates/ サブフォルダに配置することもできます。Netteは両方のバリアントをサポートしています。テンプレートを Presentation/ フォルダの外に完全に配置することもできます。テンプレートの配置オプションに関するすべての情報は、テンプレートの検索の章にあります。

ヘルパークラスとコンポーネント

Presenterとテンプレートには、しばしば他のヘルパーファイルも伴います。それらをその適用範囲に応じて論理的に配置します。

1. Presenterのすぐ隣、特定のPresenter用の特定のコンポーネントの場合:

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 に配置します。選択はチームの慣習によります。

モデル – アプリケーションの心臓部

モデルには、アプリケーションのすべてのビジネスロジックが含まれています。その整理には、再びルールが適用されます – ドメインごとに構造化します。

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)を統合します
  • アプリケーション全体に基本的なサービス(メール、データベース)を提供します
  • ほとんどの場合、特定のドメイン(製品、注文、記事)に依存しません – キャッシュやロガーはeコマースやブログで同じように機能します。

特定のクラスがここに属するか、モデルに属するか迷っていますか?重要な違いは、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経由で実行されます。HTTPリクエスト経由で実行することもできますが、セキュリティを考慮する必要があります。タスクを実行するPresenterは、例えばログインしたユーザーのみ、または強力なトークンと許可されたIPアドレスからのアクセスのみに保護する必要があります。長いタスクの場合、スクリプトのタイムアウト制限を増やし、セッションがロックされないように session_write_close() を使用する必要があります。

その他の可能なディレクトリ

前述の基本ディレクトリに加えて、プロジェクトのニーズに応じて他の特殊なフォルダを追加できます。最も一般的なものとその使用法を見てみましょう。

app/
├── Api/              ← プレゼンテーションレイヤーに依存しないAPIロジック
├── Database/         ← テストデータ用のマイグレーションスクリプトとシーダー
├── Components/       ← アプリケーション全体で共有されるビジュアルコンポーネント
├── Event/            ← イベント駆動アーキテクチャを使用する場合に便利
├── Mail/             ← 電子メールテンプレートと関連ロジック
└── Utils/            ← ヘルパークラス

アプリケーション全体のPresenterで使用される共有ビジュアルコンポーネントには、app/Components または app/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

Presenterのマッピング

マッピングは、Presenter名からクラス名を導出するためのルールを定義します。これらは設定application › mapping キーの下で指定します。

このページでは、Presenterを app/Presentation フォルダ(または app/UI)に配置することを示しました。この慣例をNetteに設定ファイルで伝える必要があります。1行で十分です。

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

マッピングはどのように機能しますか?よりよく理解するために、まずモジュールなしのアプリケーションを想像してみましょう。Presenterクラスが App\Presentation 名前空間に属するようにし、Presenter Home がクラス App\Presentation\HomePresenter にマッピングされるようにしたいとします。これは、この設定で実現できます。

application:
	mapping: App\Presentation\*Presenter

マッピングは、Presenter名 Home がマスク App\Presentation\*Presenter のアスタリスクを置き換え、結果としてクラス名 App\Presentation\HomePresenter を得るように機能します。簡単です!

しかし、この章や他の章の例でわかるように、Presenterクラスを同名のサブディレクトリに配置します。例えば、Presenter Home はクラス App\Presentation\Home\HomePresenter にマッピングされます。これは、コロンを2重にすることで実現できます(Nette Application 3.2が必要)。

application:
	mapping: App\Presentation\**Presenter

次に、Presenterをモジュールにマッピングします。各モジュールに対して特定のマッピングを定義できます。

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

この設定によると、Presenter Front:Home はクラス App\Presentation\Front\Home\HomePresenter にマッピングされ、Presenter Api:OAuth はクラス App\Api\OAuthPresenter にマッピングされます。

モジュール FrontAdmin は同様のマッピング方法を持ち、そのようなモジュールはおそらくもっと多いため、それらを置き換える一般的なルールを作成することが可能です。したがって、クラスマスクにモジュール用の新しいアスタリスクが追加されます。

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

これは、例えばPresenter Admin:User:Edit のような、より深くネストされたディレクトリ構造でも機能します。アスタリスクを持つセグメントは各レベルで繰り返され、結果はクラス App\Presentation\Admin\User\Edit\EditPresenter になります。

代替の表記法は、文字列の代わりに3つのセグメントからなる配列を使用することです。この表記法は前のものと同等です。

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