次の方法で共有


Orchard CMS

Orchard の拡張性

Bertrand Le Le

コード サンプルのダウンロード

ほとんどの Web アプリケーションには多くの共通点がありますが、同時に相違点も数多く存在します。Web アプリケーションにはそれぞれ静的ページ ("利用規約"、"会社概要" など) があります。コンテンツは共通のレイアウトに収められています。ナビゲーション メニューもあります。検索機能、コメントの投稿機能、評価機能が含まれていたり、ソーシャル ネットワークと統合されていることもあります。しかし、用途は、ブログ、書籍の販売、友人とのつながりを維持する、好みのテクノロジに関する大量の参考記事を収容するなど、それぞれ異なります。

コンテンツ管理システム (CMS) は、構築するサイトの種類に制約を設けずに、サイトの共通部分を提供することを目的としています。しかし、CMS は拡張性に若干課題があります。

Orchard CMS (orchardproject.net) の作成者 (筆者を含む) は拡張性を高めるため、組み立てと表記法に大きく依存するアプローチを採用しています。今回の記事では、システムを拡張する簡単な例をいくつか紹介します。このシステムは、読者が独自のモジュールを作成する際の出発点とするのに適しています。

動的型システム

サイトの構築にどの CMS を使用するとしても、CMS によってそれぞれ名前は異なりますが、中心となるコンテンツ エンティティが存在します。Drupal ではこれをノードと呼び、Orchard ではコンテンツ項目と呼びます。コンテンツ項目は、コンテンツのアトム (原子単位) で、ブログの投稿、ページ、製品、ウィジェット、状態の更新などを指します。コンテンツ項目には対応する URL が存在する場合も存在しない場合もあります。各コンテンツ項目のスキーマは大きく異なりますが、サイトのコンテンツの最小単位である点は共通しています。

アトムに分割する

開発者として最初に行うのは、ある範囲にとって適切になるよう、コンテンツ項目をクラスのインスタンス (投稿、ページ、製品、またはウィジェット) として特定することです。クラスがメンバー (フィールド、プロパティ、およびメソッド) から構成されるように、コンテンツの型 (コンテンツ項目の "クラス") はそれ自体がオブジェクトの複合体です。コンテンツ項目は、型 (型自体もクラス) を持つ単純なプロパティから構成されるのではなく、コンテンツ パーツ (コンテンツの動作のアトム) から構成されます。これは重要な特徴なので、少し例を紹介します。

ブログの投稿は、一般に、URL、タイトル、日付、リッチ テキスト形式の本文、タグの一覧、およびユーザーからのコメントの一覧で構成されます。これらの各パーツはどれもブログの投稿固有のものではありません。個々のパーツが独特なのではなく、各パーツの組み立て方を独特にすることでブログの投稿を特徴付けます。

ほとんどのブログ投稿にはコメントがありますが、このコメントは商用サイトでもレビューを実装するために使用されることがあります。同様に、ブログの投稿に限らず、コンテンツ項目を分類する方法として、タグが役に立つ場合があります。リッチ テキスト形式の投稿本文は、ページの本文と変わりありません。このように例を挙げたらきりがありません。このようなことから、Web サイトの動作単位はコンテンツの単位よりも小さくなることは明らかです。

CMS についてもう 1 つ重要な点は、コンテンツの型が前もって決まらないことです。シンプルなテキストが使用されていたブログの投稿も、すぐにそれだけではすまなくなりました。現在ではごく普通に、動画、ポッドキャスト、画像ギャラリーなどが含まれています。世界中を旅しながらブログを作成するのであれば、投稿した場所についての情報も必要になるでしょう。

そこで、コンテンツ パーツの出番です。緯度と経度の情報が必要なら、地図パーツを追加することによってブログの投稿の型を拡張します。地図パーツは、Orchard のモジュール ギャラリー (gallery.orchardproject.net) にもいくつか用意しています。既存の型にパーツを追加する操作について考えてみると、この操作を最もよく実行するのは、開発者ではなく、サイトの所有者であることがすぐに明白になります。そのため、Microsoft .NET Framework の型に複合プロパティを追加することしかできないのは困ります。そこで、メタデータ駆動の方法で実行時に追加できるよう、管理 UI を構築しています (Figure 1 参照)。

The Orchard Content Type Editor
図 1 Orchard のコンテンツ型エディター

これが Orchard を拡張する最初の方法です。コンテンツの型は、この管理 UI からいつでも作成および拡張できます。当然ながら、管理 UI から実行できることは、次のようにコードからも実行できます。

item.Weld(part);

このコードは、パーツをコンテンツ項目に動的に関連付けます。これは興味深い機能で、型のインスタンスを実行時に動的に拡張できます。動的言語ではこのような機能をミックスインと呼びますが、C# など、静的に型指定する言語ではほとんど耳にすることがない概念です。これにより新たな可能性が広がりますが、管理 UI から実行していたこととまったく同じではありません。また、以下に示すように、各インスタンスにパーツを追加する代わりに、型にパーツを追加できることも考えています。

ContentDefinitionManager.AlterTypeDefinition(
  "BlogPost", ctb => ctb.WithPart("MapPart")
);

これは、実際、ブログの投稿のコンテンツ型を最初の段階で定義する方法にほかなりません。

ContentDefinitionManager.AlterTypeDefinition("BlogPost",
  cfg => cfg
    .WithPart("BlogPostPart")
    .WithPart("CommonPart", p => p
      .WithSetting("CommonTypePartSettings.ShowCreatedUtcEditor", "true"))
      .WithPart("PublishLaterPart")
      .WithPart("RoutePart")
      .WithPart("BodyPart")
  );

このコード スニペットでは、ブログの投稿にタグとコメントが不足しているように思えます。これは、懸案事項を注意深く分離するもう 1 つの例です。ブログ モジュールは実際にはタグとコメントが存在しないことを認識します。タグとコメントはブログに対応するモジュールすぎません。モジュールを組み合わせるのは開発者以外の役割です。

優れたレシピ

セットアップ時に、この種のタスクを担当するレシピを実行します。このレシピは、サイトの初期構成を表す XML 記述です。Orchard には、ブログ (blog)、既定 (default)、およびコア (core) という 3 つのレシピが付属しています。以下のコードは、ブログの投稿にタグとコメントを追加する blog レシピのパーツを示しています。

<BlogPost ContentTypeSettings.Draftable="True" TypeIndexing.Included="true">
  <CommentsPart />
  <TagsPart />
  <LocalizationPart />
</BlogPost>

ここまで、コンテンツ パーツをコンテンツ項目に組み立てるさまざまな方法を紹介してきました。ここからは、独自のパーツを作成する方法について説明します。

パーツを構築する

新しいパーツを構築するプロセスを示すため、ここでは自作の Vandelay Industries モジュール (bit.ly/u92283 からダウンロード) の Meta 機能を例として取り上げます。この Meta 機能は、検索エンジンの最適化 (SEO) を目的として、キーワードと説明のプロパティを追加します (Figure 2 参照)。

The SEO Meta Data Editor
図 2 SEO メタ データ エディター

これらのプロパティは、検索エンジンが認識する標準のメタタグとして、ページの head セクションにレンダリングされます。

<meta content="Orchard is an open source Web CMS built on ASP.NET MVC."
  name="description" />
<meta content="Orchard, CMS, Open source" name="keywords" />

レコード

最初にデータをデータベースに保存する方法について説明します。厳密に言えば、データを Orchard データベースに保存しないパーツもあるので、すべてのパーツにレコードが必要なわけではありません。ただし、ほとんどのパーツはデータをデータベースに保存します。レコードは単なる標準オブジェクトです。

public class MetaRecord : ContentPartRecord {
  public virtual string Keywords { get; set; }
  public virtual string Description { get; set; }
}

MetaRecord クラスは ContentPartRecord クラスから派生します。必ずしもこのように派生しなくてもかまいませんが、不適切なコードを避けるためには間違いなく有効です。クラスには、キーワードと説明用に 2 つの文字列プロパティを用意します。実行時に使用する具象クラスを構築するための独自のロジックをフレームワークが "ミックスイン" できるように、これらのプロパティには virtual を付ける必要があります。

レコードの唯一の役割は、MetaHandler にあるストレージ メカニズムの宣言を用いて、データベースへの保存を行うことです。

public class MetaHandler : ContentHandler {
  public MetaHandler(
    IRepository<MetaRecord> repository) {
    Filters.Add(
      StorageFilter.For(repository));
  }
}

ストレージの初期化も必要です。初期バージョンの Orchard ではレコード クラスからデータベースのスキーマを推測していましたが、この推測がかけ離れたものになる可能性があるため、以降のバージョンでは、図 3 に示すように、スキーマの変更を明確に定義する、より正確な移行システムに置き換えられています。

図 3 明確に定義されたスキーマ変更

public class MetaMigrations : DataMigrationImpl {
  public int Create() {
    SchemaBuilder.CreateTable("MetaRecord",
      table => table
        .ContentPartRecord()
        .Column("Keywords", DbType.String)
        .Column("Description", DbType.String)
    );
    ContentDefinitionManager.AlterPartDefinition(
      "MetaPart", cfg => cfg.Attachable());
    return 1;
  }
}

MetaRecord テーブルは作成時に、MetaRecord クラスの表記法によって移行システムにマップできる名前が付けられます。このテーブルには、ContentPartRecord メソッドの呼び出しによって追加される、コンテンツ パーツ レコードのシステム列に加えて、レコード クラスの対応プロパティを表記することによって自動的にマップされる文字列型の Keywords 列と Description 列が追加されます。

移行メソッドには、新しいパーツを管理 UI から既存のコンテンツ型にアタッチできることを指定するコードもあります。

Create メソッドは、常に初期移行を表し、通常、移行番号として 1 を返します。この番号はモジュールの将来のバージョンを表します。開発者は UpdateFromX というメソッドを追加して、この X でそのメソッドが操作する移行バージョンを示すことができます。このメソッドでは、スキーマの新たな移行に対応する新しい番号を返します。このシステムにより、システム内のすべてのコンポーネントを、スムースかつ柔軟に、独立してアップグレードすることができます。

ここまで見てきたように、実際のパーツを表すには個別のクラスを使用します。

パーツ クラス

パーツ自体を表すのは、ContentPart<TRecord> から派生する別のクラスです。

public class MetaPart : ContentPart<MetaRecord> {
  public string Keywords {
    get { return Record.Keywords; }
    set { Record.Keywords = value; }
  }
  public string Description {
    get { return Record.Description; }
    set { Record.Description = value; }
  }
}

パーツは、利便性を考え、レコードの Keywords プロパティと Description プロパティのプロキシとして動作しますが、このようにしなくても、レコードとそのプロパティには、ContentPart 基本クラスの Record パブリック プロパティ経由でもアクセスできます。

MetaPart パーツを含むコンテンツ項目を参照するコードでは、As メソッドを呼び出すことによって、Keywords プロパティと Description プロパティに厳密に型指定したアクセスが可能になります。これは、CLR キャスト操作の Orchard 型システム版と言えます。

var metaKeywords = item.As<MetaPart>().Keywords;

パーツ クラスには、パーツのデータ固有の動作も実装します。たとえば、複合製品では、二次製品へのアクセスや合計価格の計算を可能にするメソッドやプロパティを公開することができます。

ユーザー操作に関係する動作 (標準 ASP.NET MVC アプリケーションのコントローラーに含まれるオーケストレーション コード) はまた別の問題です。これにはドライバーが関与します。

ドライバー

コンテンツ項目に含まれる各パーツは、要求ライフサイクルに参加する機会を得、事実上、ASP.NET MVC のコントローラーの役割を果たす必要があります。ただし、すべての要求に対応するのではなく、パーツに関係する要求だけに対応する必要があります。コンテンツ パーツのドライバーは、対応範囲を限定したコントローラーの役割を果たします。ドライバーにはルートからそのメソッドへのマッピングが存在しないことから、コントローラーと同じ完全な機能性を備える必要はありません。代わりに、Display や Editor など、定義済みのイベントを処理するメソッドで構成します。ドライバーは、ContentPartDriver から派生したクラスにすぎません。

Display メソッドは、Orchard がパーツを読み取り専用の形式でレンダリングする必要があるときに呼び出されるメソッドです (図 4 参照)。

図 4 パーツのレンダリングを準備するドライバーの Display メソッド

protected override DriverResult Display(
  MetaPart part, string displayType, dynamic shapeHelper) {
  var resourceManager = _wca.GetContext().Resolve<IResourceManager>();
  if (!String.IsNullOrWhiteSpace(part.Description)) {
    resourceManager.SetMeta(new MetaEntry {
      Name = "description",
      Content = part.Description
    });
  }
  if (!String.IsNullOrWhiteSpace(part.Keywords)) {
    resourceManager.SetMeta(new MetaEntry {
      Name = "keywords",
      Content = part.Keywords
    });
  }
  return null;
}

実のところ、このドライバーはやや特殊です。大半のドライバーは (すぐにその詳細を) 単純にその場でレンダリングするだけですが、Meta パーツは head セクションにメタタグをレンダリングする必要があります。HTML ドキュメントの head セクションは共有リソースのため、特別な注意が必要です。Orchard は、上記の例で示すように、こうした共有リソースにアクセスするための API を提供します。ここではリソース マネージャーを使用してメタタグを設定しています。このリソース マネージャーが、実際のタグのレンダリングを担当します。

今回のシナリオではその場でレンダリングするものがないため、メソッドからは null を返します。大半のドライバー メソッドは、シェイプと呼ばれる動的オブジェクトを返します。このオブジェクトは ASP.NET MVC のビューモデルに似ています。シェイプを HTML にレンダリングするときにもう一度シェイプについて説明します。それまでは、シェイプは非常に柔軟性の高いオブジェクトで、以下に示すように、特別なクラスを作成する必要なく、そのオブジェクトをレンダリングするテンプレートに関連するあらゆるものを表せるオブジェクトと考えてください。

protected override DriverResult Editor(MetaPart part, dynamic shapeHelper) {
  return ContentShape("Parts_Meta_Edit",
    () => shapeHelper.EditorTemplate(
      TemplateName: "Parts/Meta",
      Model: part,
      Prefix: Prefix));
}

Editor メソッドには、パーツのエディター UI のレンダリングを準備する役割があります。このメソッドは、通常、複合編集 UI を構築するのに適した、特別な種類のシェイプを返します。

ドライバーに必要な最後のメソッドは、エディターからのデータを処理するメソッドです。

protected override DriverResult Editor(MetaPart part,
  IUpdateModel updater, dynamic shapeHelper) {
  updater.TryUpdateModel(part, Prefix, null, null);
  return Editor(part, shapeHelper);
}

このメソッドのコードでは TryUpdateModel を呼び出して、エディターから返されたデータを使って自動的にパーツを更新します。この作業が完了したら、最初の Editor メソッドを呼び出して、そのメソッドが作成した同じエディター シェイプを返します。

シェイプをレンダリングする

すべてのパーツのすべてのドライバーを呼び出すことによって、Orchard はシェイプのツリー (要求全体の動的な大規模複合ビューモデル) を構築できます。次の作業は、テンプレートによってシェイプをレンダリングできるように、それぞれのシェイプをテンプレートに解決する方法を考えることです。そのためには、各シェイプの名前 (Editor メソッドの場合は Parts_Meta_Edit) を見て、システムの定義済みの場所 (現在のテーマやモジュールの Views フォルダーなど) にあるファイルへのマップを試みます。この方法により、適切な名前のファイルをローカル テーマにドロップするだけでシステム内のシェイプの既定のレンダリングをオーバーライドできるため、この方法は拡張性の点で重要です。

今回はモジュールの Views\EditorTemplates\Parts フォルダーに Meta.cshtml というファイルをドロップしました (Figure 5 参照)。

図 5 MetaPart の Editor テンプレート

@using Vandelay.Industries.Models
@model MetaPart
<fieldset>
  <legend>SEO Meta Data</legend>
  <div class="editor-label">
    @Html.LabelFor(model => model.Keywords)
  </div>
  <div class="editor-field">
    @Html.TextBoxFor(model => model.Keywords, new { @class = "large text" })
    @Html.ValidationMessageFor(model => model.Keywords)
  </div>
  <div class="editor-label">
    @Html.LabelFor(model => model.Description)
  </div>
  <div class="editor-field">
    @Html.TextAreaFor(model => model.Description)
    @Html.ValidationMessageFor(model => model.Description)
  </div>
</fieldset>

すべてがコンテンツ

拡張性の他のトピックに移る前に、コンテンツ項目の型システムを理解した後、次に理解すべき Orchard の最も重要な概念について触れておきます。システムの数多くの重要なエンティティは、コンテンツ項目として定義します。たとえば、ユーザーは、任意のプロパティをプロファイル モジュールに追加できるコンテンツ項目です。ウィジェットも、テーマが定義するゾーンにレンダリングできるコンテンツ項目です。Orchard では、このようにして、検索フォーム、ブログのアーカイブ、タグ クラウド、サイドバー UI などを作成します。しかし、驚くべきことは、サイト自体もコンテンツ項目として使用できることです。サイトの設定も事実上 Orchard のコンテンツ項目です。このことは、Orchard でマルチテナント機能が管理される方法を理解すると、大いに納得できます。独自のサイト設定を追加する場合は、パーツをコンテンツ型の Site に追加する必要があるだけです。すると、ここまで説明してきたのとまったく同じ手順に従って、管理編集 UI を構築できます。統一性があり、拡張可能なコンテンツ型システムは、非常に意義深い概念です。

拡張機能をパッケージ化する

ここまでは Orchard 向けの独自のパーツを構築する方法について説明してきました。ここで、モジュールとテーマの考え方を、これらの用語を定義することなく紹介します。ひと言で言えば、モジュールとテーマはシステムでの配置の単位です。拡張機能はモジュールとして配布され、ビジュアルなルックアンドフィールはテーマとして配布されます。

一般に、テーマは画像、スタイルシート、およびテンプレートをオーバーライドする項目の集まりで、Themes ディレクトリ以下のディレクトリにパッケージ化されます。このディレクトリのルートには、theme.txt マニフェスト ファイルもあり、テーマの作成者などのメタデータが定義されます。

同様に、モジュールは、Modules ディレクトリ以下の 1 つのディレクトリです。これは、やや込み入った ASP.NET MVC 領域でもあります。たとえば、ここには作成者、Web サイト、機能名、依存関係、バージョン番号など、モジュールのメタデータを宣言する module.txt マニフェスト ファイルが追加で必要です。

大きなサイトの唯一の領域なので、モジュールはいくつかの共有リソースと適切に連携する必要があります。たとえば、モジュールが使用するルートは、IRouteProvider を実装するクラスによって定義する必要があります。Orchard は、すべてのモジュールによって提供される情報から全ルートのテーブルを構築します。同様に、モジュールは INavigationProvider インターフェイスを実装することによって、管理メニューの構築に関与できます。

興味深いことに、モジュールのコードは、通常、コンパイル済みのバイナリとしては提供されません (技術的には可能です)。これは、モジュールのハッキングを奨励するために意図的に決めたことです。ユーザーはギャラリーからモジュールをダウンロードし、それを出発点として、ユーザー固有のニーズに対処するように調整できます。コードを調整できることは、Drupal や WordPress などの PHP CMS の長所の 1 つです。そのため、Orchard でも同種の柔軟性を提供したいと考えました。モジュールをダウンロードすると、ソース コードがダウンロードされます。このコードは動的にコンパイルできます。ソース ファイルに変更を加えれば、その変更を受けてモジュールが再コンパイルされます。

依存関係の挿入

ここまでは、拡張性の 1 つの種類として型システムに重点を置いて説明してきました。それは、型システムが Orchard モジュールの大多数を表現するためです。しかし、拡張ポイントはほかにも多数あります。事実、ここに一覧するには多すぎるほどの拡張ポイントがあります。それでも、このフレームワークで機能する汎用原理についていくつか説明を加えておきたいと思います。

高度なモジュール方式のシステムで明らかに重要な点の 1 つが疎結合です。Orchard では、低レベルのプラミング以上のほぼすべてがモジュールです。モジュールさえ、ほかのモジュールによって管理されます。こうしたモジュールをできる限り別のモジュールから独立させたいと考えるならば、つまり、ある機能実装を別の機能実装と交換できるようにしたいと考えるならば、モジュールに簡単に依存関係を持ち込むことはできません。

この目標を実現する重要な手法が依存関係の挿入です。別のクラスのサービスを使用する必要があるときは、単純にそのクラスのインスタンスを作成してはいけません。そうすると、そのクラスと密接な依存関係を確立することになります。代わりに、このクラスを実装するインターフェイスをコンストラクター パラメーターとして挿入します (図 6 参照)。

図 6 コンストラクター パラメーターによる依存関係の挿入

private readonly IRepository<ContentTagRecord> _contentTagRepository;
private readonly IContentManager _contentManager;
private readonly ICacheManager _cacheManager;
private readonly ISignals _signals;
public TagCloudService(
  IRepository<ContentTagRecord> contentTagRepository,
  IContentManager contentManager,
  ICacheManager cacheManager,
  ISignals signals)
  _contentTagRepository = contentTagRepository;
  _contentManager = contentManager;
  _cacheManager = cacheManager;
  _signals = signals;
}

この方法により、クラスではなく、インターフェイスとの依存関係になります。その結果、コードを変更しないで実装を交換できます。挿入されるインターフェイスの固有の実装は、インターフェイスのコンシューマーの決定事項ではなくなります。制御は反転し、この決定を行うのはフレームワークになります。

もちろん、このことは Orchard で定義されているインターフェイスには限定されません。すべてのモジュールは、IDependency から派生するインターフェイスを宣言するだけで、独自の拡張ポイントを提供できます。実にシンプルです。

その他の拡張ポイント

今回は拡張性の一端を紹介したにすぎません。Orchard には、創造的手法でシステムの拡張に使用できる数多くのインターフェイスが存在します。Orchard は、本質的に拡張性エンジンにほかならないと言っても過言ではありません。システムのすべての部分が交換可能かつ拡張可能です。

今回の記事を締めくくる前に、システムに含まれ、確認しておくことをお勧めする最も有益なインターフェイスをいくつか紹介しておきます。残り少なくなってきたので、すべてを説明することはできませんが、簡単な一覧を掲載し、コードを調べ、インターフェイスの用途を確認できるようにしておきます。ちなみに、これは何かを学習する際に優れた方法になります。

  • IWorkContextAccessor: コードから現在の要求の作業コンテキストにアクセスできるようにします。作業コンテキストは、HttpContext、現在のレイアウト、サイト構成、ユーザー、テーマ、およびカルチャへのアクセスを提供します。また、依存関係の挿入を実行できない場所、または構築後まで依存関係の挿入を遅延する必要がある場所からインターフェイスの実装を取得する機能も提供します。
  • IContentManager: コンテンツ項目を照会および管理するのに必要なすべての機能を提供します。
  • IRepository<T>: IContentManager では十分ではない場合に、低レベルのデータ アクセス手法にアクセスできるようにします。
  • IShapeTableProvider: 実行時のシェイプ操作のシナリオを可能にします。基本的には、シェイプに関連するイベントをフックし、特定の状況に使用する代替シェイプの作成、シェイプの変換、シェイプへのメンバーの追加、レイアウト内でのシェイプの移動などを可能にします。
  • IBackgroundTask、IScheduledTask、および IScheduledTaskHandler: バックグラウンドで実行するために、タスクを遅延または反復する必要がある場合に使用するインターフェイスです。
  • IPermissionsProvider: モジュールが独自のアクセス許可を公開できるようにします。

詳細情報

Orchard は拡張性に富んでいるため、今回の記事ではすべてを紹介することができませんでした。ここで紹介した以上の詳細を調べてみることをお勧めします。私たちが運営する友好的で愛すべきコミュニティ (orchard.codeplex.com/discussions) でガイドや疑問点の回答などをお探しください。たくさんの実装とたくさんのアイデアを公開しています。このコミュニティは重大な提案を提供する大きな機会にもなります。

Bertrand Le Roy の開発者としてのキャリアは、最初のビデオ ゲームを発表した 1982 年に始まりました。彼は、ASP.NET 上で実行される、おそらく最初の CMS を 2002 年にリリースしました。その 1 年後、Microsoft ASP.NET チームに採用され、米国に移住しました。彼は、ASP.NET のバージョン 2.0 から 4、および ASP.NET AJAX に取り組み、jQuery を .NET 開発者の公式ツールにするのに貢献しました。また、Microsoft を代表して OpenAjax Alliance 運営委員会に参加しています。

この記事のレビューに協力してくれた技術スタッフの Sebastien Ros に心より感謝いたします。