はじめに
みなさん、初めまして!リバネス開発チームのトミー(@tomyf)です。
最近弊社の求人を出すようになりましたので、少しでも開発のイメージがつきやすいように書いていきたいと思います!
弊社では、アプリケーションの一部に Laravel を使用しています。
Laravel を初期インストールした段階で、ある程度ディレクトリ分けされていますが、そこから先は開発チームの方針や開発者個人によって個性が出てくるところだと思います。
本記事では、私達の開発チームの Laravel の使い方を載せていきますので、「拙者はこうしている!」や「真似したい!」など気軽にコメントください😆
※ セキュリティの観点から、具体的なディレクトリ名やファイル名は別の名称で置き換えています。
環境
- Docker v20.10.17
- Laravel v9.25.1
app ディレクトリについて
アプリケーションのメインになる部分ですね!多くの人がコントローラにロジックを書いていくことになると思います。慣れて来るとフォームリクエストにバリデーションを分割したり、サービス層を作ったり、モデルにロジックを書いたりと工夫されたりしてますよね?
私達の開発チームでは、以下のようなディレクトリ構成にしています!
ここでは、ユーザモデル User
とプロジェクトモデル Project
があると仮定しています。
※ 変更のない Console
や Exceptions
や Providers
は省略しています。
app/
├─ Http/
│ ├─ Controllers/
│ │ ├─ WithToken/
│ │ │ ├─ ProjectController.php
│ │ │ └─ GetAuthUser.php
│ │ └─ WithoutToken/
│ │ └─ GetPublicProjects.php
│ │
│ ├─ Middleware/
│ │ └─ Authenticate.php
│ │
│ ├─ Requests/
│ │ ├─ WithToken/
│ │ │ ├─ Project/
│ │ │ │ ├─ IndexRequest.php
│ │ │ │ └─ StoreRequest.php
│ │ │ └─ GetAuthUserRequest.php
│ │ └─ WithoutToken/
│ │ └─ GetPublicProjectsRequest.php
│ │
│ └─ Resources/
│ ├─ ProjectCollection.php
│ ├─ ProjectResource.php
│ ├─ PublicProjectCollection.php
│ ├─ PublicProjectResource.php
│ ├─ UserCollection.php
│ └─ UserResource.php
│
├─ Models/
│ ├─ Concerns/
│ │ ├─ HasProjectRelationships.php
│ │ └─ HasUuidAttributes.php
│ ├─ Project.php
│ └─ User.php
│
├─ Services/
│ ├─ MediaLiveManager.php
│ └─ S3Manager.php
│
├─ UseCases/
│ └─ Project/
│ ├─ GetPublicProjects.php
│ ├─ GetProjectsByUser.php
│ └─ CreateProject.php
│
└─ ValueObjects/
└─ WhereValue.php
Controllers
コントローラですが、ユーザ認証のトークンが必要かどうかでディレクトリを区切っています。 API パスの段階で分かれるようにしているため、それに合わせてディレクトリも分けている形となっています。
基本的にリソースコントローラは WithToken
側に作成することになり、 WithoutToken
には GET
メソッドのシングルアクションコントローラを作成しています。
シングルアクションコントローラのファイル名は、関数名の命名規則に準拠していますが、もっと良い名付けがあればコメントください!
こちらはプロジェクトを新規作成する場合のコントローラの例です。
use App\Http\Controllers\Controller;
use App\Http\Requests\WithToken\Project\StoreRequest;
use App\Http\Resources\ProjectResource;
use App\UseCases\Project\CreateProject;
class ProjectController extends Controller
{
public function store(StoreRequest $request, CreateProject $createProject)
{
$user = $request->user();
$newProject = $request->makeProject();
$project = $createProject($newProject, $user);
return new ProjectResource($project);
}
}
Requests
フォームリクエストでは、ユーザが対象のファイルの権限を持っているかやバリデーションの検証を行っています。
また、リソースの作成に関する API の場合は、ここでモデルのインスタンスを作成する処理を行っています。
それぞれのコントローラに対応するようにディレクトリを区切っています。リソースコントローラの場合は、リソース名でディレクトリを作成し、シングルアクションコントローラの場合は、アクション名 + Request
を付けるようにしています。
こちらはプロジェクトを新規作成する場合のコントローラのフォームリクエストの例です。
use App\Models\Project;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest extends FormRequest
{
public function rules()
{
return [
'name' => ['required', 'string']
];
}
public function makeProject()
{
return new Project($this->validated());
}
}
Resources
コントローラのレスポンスは、全て API リソースを通して返すようにしています。 ProjectResource
と PublicProjectResource
がありますが、これは本人にしか見れない情報が別のユーザに表示されないように、明示的に分けています。
こちらはプロジェクトモデルを基に作成した API リソースの例です。
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Arr;
class ProjectResource extends JsonResource
{
public function toArray($request)
{
return [
...Arr::only(parent::toArray($request), [
'id',
'uuid',
'user_id',
'name',
]),
'user' => new UserResource($this->whenLoaded('user')),
];
}
}
Models / Concerns
この Concerns
が気になっている人もいるのではないでしょうか。ここはモデルの機能を拡張する trait
を定義したファイルを入れています。ここでは2種類のファイルがあり、一つは「モデルに UUID があることを定義し、新規作成時に自動で生成して格納する」と言った汎用的なものと、「特定のモデルに関連するリレーションを定義する」と言った専用のものがあります。
汎用的な trait
がこちらです。
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
trait HasUuidAttributes
{
public static function bootHasUuidAttributes()
{
static::creating(function (Model $model) {
$model->generateUuid();
});
}
public function generateUuid()
{
$uuid = Str::orderedUuid()->toString();
$this->setUuid($uuid);
}
public function setUuid($value)
{
$this->{$this->getUuidColumn()} = $value;
return $this;
}
public function getUuidColumn()
{
return static::UUID;
}
}
プロジェクトモデル専用の trait
がこちらです。
use App\Models\User;
trait HasProjectRelationships
{
public function user()
{
return $this->belongsTo(User::class);
}
}
trait
の使用方法はこのように記載します。
use App\Models\Concerns\HasProjectRelationships;
use App\Models\Concerns\HasUuidAttributes;
class ProjectEntry extends Model
{
use HasUuidAttributes, HasProjectRelationships
const UUID = 'uuid';
}
Services
ここでは一般的に言われるサービス層とは少し違う意図で作成しています。
主に外部サービスとの連携に関する処理をまとめたファイルを配置しています。直接データベースに接続してデータを取得する処理はありません。
UseCases
このユースケースに、データベースからデータ取得やデータ作成を行う処理を書いていきます。ドメイン毎にディレクトリを区切って、全てシングルアクションメソッドで作成しています。
use App\Models\User;
class GetProjectsByUser
{
public function __invoke(User $user)
{
$projects = $user->projects()->latest()->paginate();
$projects->load('user');
return $projects;
}
}
use App\Models\Project;
use App\Models\User;
class CreateProject
{
public function __invoke(Project $project, User $user)
{
$user->projects()->save($project);
$project->load('user');
return $project;
}
}
ValueObjects
バリューオブジェクトには、システム全体で共通して使用したいデータ形式を定義しています。主な用途としては、データ取得時の条件をまとめた配列をユースケースの引数に渡したい時に使用します。
おわりに
今回は Laravel のディレクトリ構成について紹介でした!
これから新しいプロジェクトで Laravel を使うか迷っている方や Laravel を使ってプロジェクトを立ち上げる予定の方の助けになれば嬉しいです。
開発が進んでいけば、ディレクトリ構成が変わる事もあると思いますので、その都度更新していきたいと思います。
こちらのディレクトリ構成に辿り着くまでに @mpyw さんの 5年間 Laravel を使って辿り着いた,全然頑張らない「なんちゃってクリーンアーキテクチャ」という落としどころ がとても参考になりました!