Laravel4、ディレクトリー構造はお気にめすまま

タグ: Laravel4  

この記事は書きかけです。適宜、変更/追加していきます。

Laravel4のWebアプリで使用するディレクトリー構造は、app下にまとまっています。

しかし、自分の好みに合わせ自由に変更することができます。

例えば、単にデフォルトの構造自体が気に入らないのかも知れません。他のMVCモデルを採用しているフレームワークと合わせているだけですからね。

Laravel4の上で動作するアプリには、app下にプロジェクトのディレクトリーを作成し、そこに構成するものがあります。開発者の好みに合わせた構成にするためです。

同じアプリを別々の部署で開発するため、スパっと分けたいのかも知れません。そうした時のヒントです。

注意:この様な記事を書くと、「Laravelは標準の構成を変更しないと使えない」など、揚げ足をとる情報が発信されるようです。ですから、予防線を張っておきましょう。

学習段階、小さなアプリ、使い捨てのツールなどの作成であれば、デフォルトのまま使用するのが正しいでしょう。余計な手間をかける必要がありません。多分、中程度のアプリ程度くらいまでであれば、どうにか作成できるでしょう。

しかし、規模が大きくなるにつれ、構成の自由度がないのは欠点になります。開発しづらくなるからです。それはディレクトリー構造が固定されたフレームワークで作成した時に、多くの開発者が感じることでしょう。理由についてはLaravel開発者のTaylor氏が著書の「Laravel:見習いから職人へ」の中で、一因に触れています。

この記事は、比較的規模の大きなアプリケーションを構築する場合に、「こんなこともできますよ」と方法を紹介することを目的としています。やり方を強制するものではありません。開発の自由度を増すためにLaravelに用意されている仕組みを紹介するものです。

独立させる

ある機能を独立させ、再利用したい場合は、Composerのパッケージとして開発しましょう。多くの人に使用してもらうため、Packagistに登録することもできます。また、個人や会社内だけでローカル使用することもできます。パッケージの存在場所を指定してあげれば、ローカルでもComposerは活躍します。

Composerのパッケージとして開発するには、Laravelのワークベンチ機能を利用しましょう。コマンドで、開発環境を整えてくれます。

例えば、大きな会社でそれぞれ担当チームが付き、全く独立した機能を分けて開発したいなんて場合も、(そこまで大きければ、Laravelをフレームワークとして選択するかは疑問ですが)パッケージとして開発するのが第一の選択肢です。

もうひとつはパッケージとしなくても、ディレクトリーを分け、PSR-0規約に基づき、名前空間を分けて開発する方法です。一人で開発するが、独自構成でやりたい場合も、こちらですね。

パッケージの場合も、単に名前空間でディレクトリー構成を分ける場合も、手間としては余り代わりません。ワークベンチを--resourcesオプション付きで生成した環境でパッケージを利用するなら、ビュー、言語、設定のリソースにアクセスするために、パッケージ名を二重のコロンで指定できるように、予め設定されます。(ワークベンチを利用しなくても、独立してパッケージを開発するのは、さほど難しくありません。)

ワークベンチでの開発は、そのうちどなたかが、書いてくれると思います。ですから、今回はPSR-0を使い、ディレクトリー構成を自分好みに合わせるやり方を紹介します。

app下か?ルート下か?

作成するとしたらappディレクトリー下か、もしくはappと同じレベルのプロジェクトルートディレクトリーに作成するかでしょう。

どちらでも構いません。どちらでもお好きなほうへどうぞ。

今回はルート下にSashimiディレクトリーを作成することにしましょう。

composer.jsonの設定

PSR-0により、名前空間でクラスをロードできるように設定します。autoloadセクションに追加します。

    "autoload": {
        "psr-0": {
            "Sashimi\\": ""
        },
        "classmap": [
            "app/commands",
            "app/controllers",
                        ...

名前空間Sashimiを利用し、インストールディレクトリー下のSashimiディレクトリーからクラスをロードする指定です。もし、ディレクトリーをapp下に作成した場合は、"Sashimi\\":"app"になります。

Sashimiのディレクトリー構成

私の趣味に合わせて、ズラズラと並べました。

Sashimi
   +Commands
   +Configurations
   +Controllers
   +EloquentModels
   +Languages
      +en
      +ja
   +Migrations
   +Repositories
   +Seeds
   +Services
      +ServiceProviders
         -SahimiServiceProvider.php
   +Tests
      +UnitTests
   +Views
   -routes.php

サービスプロバイダー

サービスプロバイダーは名前は長いですが、「準備するクラス」と考えれば、理解は早いです。準備が必要なコードをここに集めましょう。今回の例のように、一つにまとめても良いですし、初期設定としてやることが多いのであれば、分割してもよいのです。これも、お好みでどうぞ。

SashimiServiceProvider.php

<?php

namespace Sashimi\Services\ServiceProviders;

use Illuminate\Support\ServiceProvider;

class SashimiServiceProvider extends ServiceProvider
{

    public function register()
    {
        // IoCコンテナ登録のみ行うことが推奨されています
    }

    public function boot()
    {
        // Laravel用クラスローダーの登録
        \ClassLoader::addDirectories( array(
            __DIR__.'/../../Commands',
            __DIR__.'/../../Controllers',
            __DIR__.'/../../Models',
            __DIR__.'/../../Seeds',
        ) );

        // ビュー、言語、設定の名前空間(プレフィックス)設定
        \View::addNamespace( 'Sashimi', __DIR__.'/../../Views' );
        \Lang::addNamespace( 'Sashimi', __DIR__.'/../../Languages' );
        \Config::addNamespace( 'Sashimi', __DIR__.'/../../Configurations' );

        // ルート定義の読み込み
        require __DIR__.'/../../routes.php';
    }

}

ビュー、言語、設定の名前空間とは、それぞれのクラスのaddNamespaceメソッドで指定することで、Sashimi::プレフィックスで探すディレクトリーを指定します。

例えば、Sashimi/Views/User下のindex.phpビューファイルを表示するなら、コントローラーアクションかルートの中で、return View::make('Sashimi::User.index');と指定できるわけです。

Sashimiプロジェクトで利用するルートはShashimi/routes.phpで定義し、それをプロバイダーで読み込んでいます。どこで読み込むかも、自由です。例えば、app/routes.phpを代表ルート定義ファイルと意味づけ、その中からrequireするのも一手でしょう。もちろん、ファイルを分けずに、app/routes.phpでルート指定は全部直接行なっても構いません。管理のしやすさで決めればよいだけです。

いずれにせよ、ルート定義が被らないようにしましょう。(通常はURIを分けることで、問題を簡単に防ぐことが可能ですね。)

ClassLoader::addDirectoriesは一応全部指定していますが、現時点でもこれが良いのか、私自身考えがまとまっていません。もう少し、工夫してから、結論を出したいと思います。一応、やり方だけ、紹介しています。

クラスローダーにディレクトリーを登録する利点は、その下が名前空間のトップのように利用できることです。つまり、ディレクトリー直下であれば、名前空間を指定せず、クラスにアクセスできることです。

しかし、既にPSR-0の規約下にあるディレクトリーに、別のオートローディングルールを適用するのが好ましいかどうかです。これは、意見が分かれそうです。

もし上記のように、追加のLaravel式のロードディレクトリーを指定する場合は、composer.jsonにも追加することを、考えて下さい。わずかでしょうがロードのスピードアップに貢献します。(要は、composer dump-autoloadされた時に、クラスマップが作成され、そこに登録されるために早くなります。)ただし、パッケージを利用する場合は、空間名をつけないクラスをクラスマップに入れるのは避けましょう。

Laravelのクラスローダーに登録せずとも、ほとんどの場合、支障は無いようです。その実例を紹介していきます。

先ずコントローラーですが、名前空間付きのクラスをPSR-0でロードできますので、全く問題はありません。(多少長くなります。慣れの問題です。私は最近、こちらのほうが好みです。)

Route::get('login', 'Sas1himi\Controllers\AuthController@showLoginForm');

シーディングは、コマンドのデフォルトで指定されるクラスが、app/database/seeds/DatabaseSeeder.phpファイルのDatabaseSeederクラスです。

この代わりになるものを`Sashimi/Seeds'の下に作成し、シーディングコマンド実行時に、明示的にそのクラスを指定できます。

php artisan db:seed --class="Sashimi\Seeds\DefaultSeeder"

長いですね。こんな時は、Bash使用者であれば、エイリアスを切っておきましょう。

alias dbseed='php artisan db:seed --class="Sashimi\Seeds\DefaultSeeder"'

Laravel3の時にEloquentモデルをPSR-0規約の構造に移したら、関係付でとても苦労しました。結局、PSR-0下に持ってくるのは諦めました。Laravel4では、リレーションに名前空間を付けたクラス名を指定することで、可能なはずですが、まだ未検証です。(すでに多くのサンプルも公開されていますし、パッケージでも使われているので問題ないと思っています。)後ほど、方法をここにまとめます。

コマンドに関しては、Laravel4になってから、作成したことがないため、未検証です。多分、問題は無いと思いますが、近々使用することになりますので、その後にご報告いたします。

テスト

PHPUnitだけを使用するのであればともかく、他のツールを利用するとなると、Tests下にさらにテスト種別ごとにディレクトリーを作成しておいたほうが便利です。今回はユニットテスト用に"UnitTests"ディレクトリーを追加しています。

テストはLaravelのクラスではありませんから、Laravelのローダーの指定には入れません。その代わりcomposer.jsonとphpunit.xmlで指定します。

Laravelのオリジナルなcomposer.jsonと同様、クラスマップにTestCase.phpへのパスを追加して下さい。

次に、phpunit.xmlで、ユニットテストが存在する、親のディレクトリーをテスト対象のディレクトリーとして追加指定します。下のように、該当する一行を付け加えてください。

    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./app/tests/</directory>
            <directory>./Sahimi/Test/UnitTests/</directory>
        </testsuite>
    </testsuites>

ご覧の通り、UnitTestsディレクトリーを追加しただけです。これで、UnitTests下の*Test.phpファイルは全部(再帰的にも)phpunitコマンドでテストされます。

パッケージの場合は、TestCase.phpをテストディレクトリーにコピーして利用したくなります。これをcomposer.jsonのクラスマップでロードすると、パッケージ単体でテストする時は支障はありませんが、公開したパッケージを使用する側でトラブルになり得ます。PSR-0で読み込まれるディレクトリー下に設置し、対応した名前空間を付けるか(推奨)、もしくはLaravel独自のクラスローダーで読み込まれることをおすすめします。

マイグレーション

マイグレーションは、デフォルトでapp/database/migrations下のファイルを対象としています。これは、migrateとmigrate:makeが対象です。

migrateとmigrate:makeを行う場合、デフォルト以外のフォルダーで実行する必要がある場合は、オプションで指定します。

一番汎用的なのは、--pathオプションで、その名の通りパスを指定します。今回の構成であれば、以下のようなオプションになります。

php artisan migrate:make create_users --path="Sashimi/Migrations"
php artisan migrate --path="Sashimi/Migrations"

もし、ワークベンチでパッケージを開発中であれば、そのワークベンチ名を--benchオプションで指定します。

Composerでインストールしたパッケージに含まれるマイグレーションを対象にする場合は、--packageで指定します。

たいてい、パスを付ける時は続けてこれらのオプションを指定するものですから、Bashシェルでフローを良くしましょう。例えば、次のようなコードを.bashrcに入れておけば、指定しやすくなります。

setpath()
{
  if [ $# -eq 1 ]
  then
    LARAVEL_MIGRATE_PATH="--path=\"$1\""
  else
    echo $LARAVEL_MIGRATE_PATH
  fi
}

setpackage()
{
  if [ $# -eq 1 ]
  then
    LARAVEL_MIGRATE_PATH="--package=\"$1\""
  else
    echo $LARAVEL_MIGRATE_PATH
  fi
}

setbench()
{
  if [ $# -eq 1 ]
  then
    LARAVEL_MIGRATE_PATH="--bench=\"$1\""
  else
    echo $LARAVEL_MIGRATE_PATH
  fi
}


setdefault()
{
  LARAVEL_MIGRATE_PATH="--path=\"app/database/migrations\""
}

setdefault

migrate()
{ 
 php artisan --ansi migrate $LARAVEL_MIGRATE_PATH $*
}

migratemake()
{
  target=$1
  shift
  php artisan --ansi migrate:make $target $LARAVEL_MIGRATE_PATH $*
}

作りたてで、あまりよくテストしていませんが、使えると思います。setpath、setpackage、setbenchはその名の通り、オプションをセットします。例えばパスに"Sashimi/Migrations"をセットするには:

setpath "Sashimi/Migrations"

これで、オプションがセットされます。

マイグレーションを行う場合は、migrateとタイプすれば、指定したパスに対して実行されます。

同様に、マイグレーションを作成する場合は、migratemake マイグレーションクラス名です。

コマンドはスクリプト中の関数名を変更し、お好きな名前にしてください。migrateが長ければmi、migratemakeはmimとか、好みに合わせ工夫しましょう。

マイグレーション対象をデフォルトのapp/database/migrationsに戻したい場合は、setdefaultです。

重要:マイグレーションには名前空間は使用できません。マイグレーション実行時は可能ですが、ロールバック時にクラスが見つからないエラーになります。管理テーブルには名前空間が保存されないからです。

そのため、マイグレーションを名前空間無しでもロードできるように、Laravelかcomposerのローダーで、読み込まれるようにしましょう。

そして、ドキュメントに書かれている通り、クラス名がバッティングしないようにパッケージならその名前などをマイグレーションクラス(ファイル名も一致させる必要があります)の先頭に付ける考慮をしましょう。

モデル

モデルはEloquentモデル用のディレクトリーを作成し、そこで管理できます。Eloquent以外の「モデル」は適切なディレクトリーを作成し、そこで管理します。Laravel開発者のTaylorさんが著書の中でも言っています通り、MVCを意識しすぎるため、VC以外を何でもかんでもモデルに突っ込むと、意味合いが違ってしまいます。MVCの弊害です。

モデルを名前空間の管理下に置いても、問題はないようです。もちろん、モデルの指定時に、名前空間を付けたクラス名を指定することになります。

その中でもUserモデルを認証で使用している場合、auth.phpの中で名前空間を付けたクラス名を忘れずに指定してください。

(まだ書きかけです。余り信用してはいけません。軽く、参考にするだけにして下さい。えっ、もともと信用していないって?;D)