Laravel3、クラスのディレクトリー構造をひと工夫

タグ: Laravel3  

今回の記事は、Laravel3でクラスファイルをどう配置するかという内容です。Laravelでだらだらと書くのは気持ちが良いですが、より複雑なWebアプリを作りたい、よりバグの少ないサイトにしたいと思った時には多少クラスの構造に注意を払う必要が出てきます。そんな時にヒントにしてもらえれば幸いです。

さて、Laravel3の公式ドキュメント中で一箇所だけ、突然毛色の変わった説明がされている箇所があります。モデルとライブラリーページのペストプラクティスのセクションです。

この説明が理解出来なくても私達のせいではありません。フォーラムでもどの様に取り扱うべきか、実際にはどう実装すべきなのかという質問が時々されます。意見は様々ですが多い意見は、1.説明されているように実体(entities)はわざわざ作成しない。サービス(services)とリポジトリー(repositoies)は作成する。2.プロジェクト専用のディレクトリーを作成することもない。modelsディレクトリーを使用すれば良いと言うところでしょうか。

バラバラなのは当然で、前提知識や環境が異なるのですから、「ベスト」と思える方法もバラバラです。前提なしにこのような話を論じた所で結論は出ません。そして、各開発者が唱えているプログラム構築の「主義」も異なります。

私自身は設計の主義にはこだわりません。ですがLaravel3でちょっと大きなアプリを作成し、テストも真面目にしようと思ったとき、やはり小さなアプリを作成する時と同じ「お気軽な」手法では開発が進まなくなることに気づき、色々な手法をしぶしぶ取り入れています。

models以外にプロジェクトディレクトリーを作成するか

LaravelはMVCパターンに基づき設計されています。それぞれに関わるファイルはmodels、views、controllersディレクトリーの下に設置されます。

ベストプラクティスの章ではプロジェクトに対して新しいディレクトリーを作成し、そこにプロジェクトファイルを設置するようにすすめています。さて、modelsディレクトリーをそのまま使ったほうが良いのでしょうか。それともプロジェクで使用するクラスのディレクトリーは別に作成し、そちらを使用したほうが良いのでしょうか。

ORMのEloquentを使用する場合、最初に考慮すべきことはEloquentモデルはmodelsディレクトリー直下に配置すると設定が楽で簡単に使用できることです。

Laravelでも、他のフレームワークでも、規約や約束事の範囲内であれば設定の手間を省くことができます。これはORMのEloquentに対しても同様です。

Eloquentモデルはmodelsディレクトリー下にサブフォルダーを作成せず、ディレクトリー直下にPHPファイルを置いて使用する場合が、一番記述が簡単でお気軽に使用できます。リレーションを使用する場合は、特にサブディレクトリーを使用すると面倒になるため、原則はmodels直下に配置するのが使いやすいのです。

そのためちょっと開発規模が大きくなると、models直下はEloquentモデルで一杯になります。コントローラーへコードが集中することを避けるために、コードをクラスにばらす場合、modelsディレクトリーに配置するのがMVCパターンなら普通だと私達は考えがちにですから、そうしたEloquentモデル以外のクラスも当然のこととして、さらにmodels下に配置します。(なにせ、デフォルトのオートロードの対象ディレクトリーですしね。)

すると、どうしてもmodelsディレクトリーは混雑します。混雑を避けるためにはサブディレクトリ−を使用します。サブディレクトリーを使用するとクラス名に下線を使用し階層構造を入れた長い名前を使用することになるか、もしくは名前空間を使用することとなります。(オートローディングの規則によります。)

ですから、modelsディレクトリーに全クラスを突っ込むのか、それとも別に作成したディレクトリーを使用するのかを決めるのは、プロジェクトの大きさをまず考慮して決めるのが良いでしょう。大きなプロジェクトでEloquent ORMを使用する場合であれば、modelsディレクトリーはEloquentモデル専用にしてしまったほうがすっきりします。

次に考慮するのは、modelsディレクトリーをEloquentモデル専用とした場合、その他のクラスを配置するため、modelsやlibrariesディレクトリーと同じ規約で使用するか、それともPSR-0規約で使用するかです。

最初にmodelsやlibrariesディレクトリーと同じ規約(ベンダー名なしのPSR-0)でオートロードさせる方法を紹介します。start.php中の定義に、プロジェクトのクラスを配置するディレクトリー名を指定します。

Autoloader::directories( array (
    path( 'app' ).'myproject', // プロジェクトディレクトリー
    path( 'app' ).'models',
    path( 'app' ).'libraries',
) );

ローディング時、配列の記述順にチェックされます。上記の例の場合、名前空間なしのAppleクラスは、まずmyprojectディレクトリーの中でapple.phpファイルの存在がチェックされ、続いてmodelsディレクトリー、最後にlibrariesディレクトリーがチェックされます。もちろん、見つかった時点でロードされます。

ですから、どのディレクトリーを優先にしたいかなどを考慮し、記述順を決めましょう。スピード優先であれば、ファイルの存在チェックのためにアクセスができるだけ起きないようにしましょう。クラスがヒットする確率が高い方を上にしたほうが、わずかにスピードアップにつながります。(通常はさほど差が出ないでしょうが、気にする方は気にするようです。)

この方法で登録する場合、ディレクトリー名、ファイル名は全部小文字にします。つまり名前空間とファイル名中に使用されている英文字は全部小文字に変換されてから、ファイルチェック、ローディングされます。

もうひとつの方法はPSR-0準拠、つまり名前空間のトップレベルはベンダー名が付いているものとして登録する方法です。

Autoloader::namespaces( array (
    'MyProject' => path( 'app' ).'myproject',
) );

ベンダー名としてMyProjectを使用しています。ですからクラスを修飾する名前空間のトップレベルがMyProjectであればmyprojectディレクトリー下だけがクラスの読み込み先に決定され、ファイルの存在のチェックは2回だけ起き、存在していれば読み込み、存在していなければクラスが存在していないというエラーになります。(先に紹介した規約のチェックのため、まず小文字のディレクトリー/ファイル名でチェックされ、次にPSR-0規約のためにチェックされます。そのため、ファイルの存在チェックが2回発生します。)

私の個人的な趣味では、このnamespces()で登録しベンダー名を先頭に付けるPSR-0が好みです。名前空間でディレクトリー位置を表すのはシンプルで分かりやすいからです。しかも、名前空間とクラス名で使用した英文字の大文字小文字はファイル名でもそのままです。SuperJob\MagicalProgramerクラスであれば、指定ディレクリー下のSuperJobサブディレクトリー中にあるMagicalProgramer.phpファイルに記述します。このように名前が長くなる場合、大文字小文字がそのままファイル名にできる方が、はるかに認識しやすいからです。しかし、皆さんが個人で開発しているなら、これは完全に趣味の世界です。どちらでも使いやすい方を選択してください。

もちろんEloquentを使用しないとか、プロジェクト全体が小さくmodlesディレクトリーがクラスファイルで雑多にならないのでしたら、こうした手間をかける必要はありません。お気軽にmodelsディレクトリー一つで開発しましょう。

リポジトリー

ベストプラクティスの章で説明されているリポジトリーとは、「読み込み先」を表す抽象的なクラスのことです。

Laravelを始めとして大半のフレームワークではORMやアクティブレコードで扱うデータベースの種類を設定で切り替えられるようになっています。リポジトリークラスを導入する利点として述べられている「ストレージの切り替え可能」とは、複数のデータベースエンジンに加え、ファイルやmemcachedのようなデータベース以外の形態にも対応できるように組めるよと言う意味です。

直接I/O系の関数を使用しないというのは大昔からベストプラクティス的に言われてきました。アセンブラで組んでいた時代からフックを一枚噛ませておくと、いざという時の拡張に役に立つと言われ続けています。ただ、大昔は関数を一枚かぶせるのも速度的やメモリの犠牲が現代と比べて大きかったので、今と同様に重要視されることは少ないプラクティスでした。

利点として紹介されているストレージを変更するという事態は、きちんと設計されているプロジェクトであれば、さほど頻繁には起きません。リポジトリークラスの導入は、テストが行いやすくなるという別の利点もあります。

以前にEloquentモデルを置き換えるというLaravel3の記事を書いたのですが、この文章中に「Eloquentモデルをハードコードしてしまうと、テストスタブに置き換えられない。」と書きました。それはどこかに書かれていたのが頭の片隅に残っていたためで、実は置き換えることが可能であると後から気が付きました。同じクラス名であればクラスエイリアスとして登録している名前のほうがmodelsディレクトリーに登録されているEloquentモデルの存在チェックよりも先に行われるため、Autoloader::alias()で対象のEloqeuntモデルをエイリアスとして指定してしまえば、Eloquentモデルでも、他のクラスでも置き換え可能です。とはいえ、Autoloadクラスの実装コードに依存する裏技的な方法です。

どの方法で置き換えするにせよ、コントローラー中にEloquentをバンバン書いてしますと、単体テストが複雑になってしまいます。テスト方法を考えるほうが、本来のコーディング対象のロジックを考えるより、難しい作業になります。そうなると、テストコードを書くのは面倒になり、結局単体テストは行わないことになりがちです。ですから、コントローラにはEloquentを含め、ストレージの操作を直接書かないこと、テストしやすいようにクラスもメソッドも単機能になるまで分割することは良いアイデアです。

テーブル操作を含めた情報の保管・引き出し作業をリポジトリーという中間クラスに代行させるのはさほど手間ではありません。実行結果をただリターンするだけなら簡単です。手間がかかるのは、いつも情報の保存や読み出し作業が成功するとは限りませんので、その実行結果を呼び出し元のコントローラーに伝達する方法をコードに組み込む必要がある点です。

伝統的にリターンコードで知らせる方法がありました。

function update($id) {
    $apple = Apple::find($id);
    if ( $apple === null ) {
        return 1; // IDが見つからない
    }
    $val = $this->valid()
    if ( $val->fails() ) {
        return 2; // バリデーションエラー
    }
    $apple->fill(Input::get());
    $ret = $apple->save();
    if ( $ret != 1 ) {
        t
    }
    return 0; // 正常完了
}

これでも良いのですが、クラス/メソッドの呼び出しがネストしている場合、全てのレベルでリターンコードのチェックコードが入るため、煩雑になります。もう少し現代的なコーディングにするならば、例外を使用しましょう。

  • 参照:[Laravelで活かす例外](/97/Laravel%E3%81%A7%E6%B4%BB%E3%81%8B%E3%81%99%E4%BE%8B%E5%A4%96 , 'load')
function update($id) {
    $appleEloquent = IoC::resolve('Apple');
    $apple = $appleEloquent->find($id);
    if ( $apple === null ) {
        throw new IdNotFoundException;
    }

    $inputs = Input::only(array('size'));

    // Appleクラスのバリデーション実行
    // バリデーションが通らない場合は、例外発生
    // ここではその例外を補足しない。

    $appleValidator = IoC::resolve('AppleValidator');
    $appleValidator->updateValidate($id, $inputs);

    // try...catchで例外を補足しないので、
    // これ以降のコードが実行されるのは
    // バリデーション通過時のみ。

    $apple->fill(Input::get());
    $ret = $apple->save();
    if ( $ret != 1 ) {
        throw new UpdateFaildException;
    }
}

この例ではバリデーションはバリデーション専用のクラスで行なっております。もちろん、リポジトリーからバリデーションを呼び出すのか、別のクラスがコントロールするのかなど、考慮すべき点はあるでしょうが、今回はリポジトリークラスからバリデーションを実行しています。

クラスを直接ハードコードしてしまうと、置き換えが面倒になります。せっかく、Laravelには手軽なIoCコンテナが実装されているので、それを利用しましょう。Apple EloquentモデルとバリデーションクラスのインスタンスをIoCコンテナで取得します。

リポジトリークラスが例外でコードのノーマルルート以外の状況を知らせているのと同じように、バリデーションクラスは例外でバリデーションに失敗した状況を知らせます。リポジトリークラスではこの例外をスルーします。これにより、バリデーション実行以降のコードには、バリデーションに成功した場合のみ制御が移ってきます。バリデーション失敗の場合は、以降のコードには制御が移りません。リポジトリクラスを呼び出した側のコードで、バリデーション失敗例外をcatchで待ち受けているはずですので、そちらへ制御が移ります。

リターンコードやフラグなどでクラス遷移を制御するよりは、例外を上手く利用したほうがすっきりとしたコードになります。

例外

例外はそれだけでディレクトリーを独立させる方法と、例外を投げるクラスと一緒のディレクトリーと設置する方法があります。

一般的にフレームワークですとそれを投げるクラスと同じ場所、もしくは階層的に近い場所へ設置されることが多いようです。

私は、まとめて独立させたディレクトリーに例外の階層に沿って配置するのが好みです。

サービス

Eloquentモデル、リポジトリークラス、例外クラス以外の要素は全部サービスです。バリデーションクラスもバリデーションサービスを提供しているクラスですのでサービスの一種という事になります。

主義により更に細かく分けることもできます。お好みでどうぞ。

多種多様なサービスが考えられるため、具体的なコードは書きません。ここでは、Eloquentとリポジトリー、例外以外のクラスは全部サービスのカテゴリーに入ると理解してください。

名前空間・ディレクトリー構成

リポジトリーとサービスの考えを取り入れるとして、実際のディレクトリー構成はどうなるのでしょうか。modelsディレクトリーだけで行うのであれば、以下のようになるでしょう。

+application
    +models
        +exceptions
            +programerrorexceptions
                -programerrorexception.php
            +executionerrorexceptions
                -executionerrorexception.php
        +repositories
            -validatorclassnotfoundexception.php
            -applerepo.php
            -orangerepo.php
            -bananarepo.php
        +services
            +validators
                -validationfaildexception.php
                -applevalidator
                -orangevalidator
                -bananavalidator
        -apple.php
        -orange.php
        -banana.php
  • exceptionsディレクトリーは例外の基底クラスを定義しています。名前通りプログラムエラーと実行エラーです。
  • repositoriesディレクトリーはリポジトリークラスです。この場合、リポジトリークラス内でバリデーションをチェックし、その実態はべつのバリデタークラスが担当すると仮定しました。バリデタークラスが無い場合はプログラムエラーとしてValidataorClassNotFoundException例外を投げることとし、そのクラスを同じディレクトリー内に設置しています。
  • servicesディレクトリーは各種機能を実現するクラスです。機能毎に更にサブディレクトリーを作成しクラスを設置するマイ規約としています。バリデーションが通らなかった場合に投げる、実行時例外としてValidationFaildExceptionを同じディレクトリーに設置しています。

今度は私の嗜好・嗜好に従い、構成してみましょう。modelsはEloquent専用にします。プロジェクトはプロジェクト名をベンダーとして扱います。今回は'ProjectH'とします。例外はexception下にまとめます。

+application
    +models
        -apple.php
        -orange.php
        -banana.php
    +ProjectH
        +Exceptions
            +ProgramErrorExceptions
                -ProgramErrorException.php
                -ValidatorClassNotFoundException.php
            +ExecutionErrorExceptions
                -ExecutionErrorException.php
                -ValidationFaildException.php
        +Repositories
            -AppleRepo.php
            -OrangeRepo.php
            -BananaRepo.php
        +Services
            +Validators
                -AppleValidator
                -OrangeValidator
                -BananaValidator

そしてstart.phpファイルでProjectHで始まる名前空間はapplication/ProjectHディレクトリー下からオートロードするように登録します。

Autoloader::namespaces( array (
    'ProjectH' => path( 'app' ).'ProjectH',
) );

今回のコードも実働させたものではなく、この記事の説明のために直接書いたものであることにご注意ください。動かない部分も含まれているかも知れません。

次回は上記の構造に従い、いくつかのクラスの実装コードを見て行きましょう。