
新卒入社からLaravelに1年くらい触れてきて、Laravelには(使いこなせないくらい)たくさん便利な機能がある一方で、ちゃんとその性質を理解して使っていかないといけない部分もあるということも知りました。
本記事ではLaravelの便利なポイントと、合わせて押さえておきたい注意事項について、主にEloquent ORMを中心にまとめていきたいと思います(今回はEloquent ORM)。
Laravelは、Webアプリケーションのためのフルスタックフレームワークです。
フルスタックというのは、Webアプリケーションを構築するために必要な要素はLaravelをインストールするだけでほぼ全て揃う、ということです。同じようにPHP向けのWebフレームワークとしてSymfonyというフレームワークも存在していますが、こちらは標準機能が最低限のものに絞られており、必要なものを逐次Composerでインストールする様な形になっています。
両者の具体的な違いを知りたい方はこちらの動画も参考になります。
PHP Conference Japan 2023: 25分で理解する!Symfonyの魅力とその実践的活用法 / 角田 一平
Laravelが前提としてさまざまな機能を包含していることを踏まえて、以下では特に抑えていきたいLaravelのコアなポイントと注意点を合わせて紹介していきます。
LaravelはActive RecordパターンのORMとしてEloquent ORMを包含しています。
Eloquent ORMは基本的にテーブルとモデルクラスを1対1で対応させて、コードベース上からのデータベースアクセスを簡潔に表現するものになっています。
// 例:Userテーブルに対応したUserモデルからid = 1のユーザーを取得する例
$user = User::find(1);
echo $user->name;
便利なポイント
ここで押さえておきたいのが、LaravelはこのEloquentモデルを使うことによりMVC的な一連のCRUD処理を簡潔に表現できるようなさまざまな機能を提供していると言うことです。
例として、ここではProduct(製品)の登録・取得・更新・削除処理を生成してみます。
Laravelではartisan make
コマンドを利用してさまざまなファイルを自動作成することができますので、ここではモデルに関する一通りのファイルを生成するオプションを使ってファイルを作成していきます。
bash
php artisan make:model Product --all
すると以下のようなファイル群が生成されます
ここで生成されたProductControllerに対してルーティングをマッピングするために以下のコードを追加します。
routes/web.php
Route::resource('products', ProductController::class);
これで現状のルーティングを確認するコマンドphp artisan route:list
を打つと以下のようにCRUDに対応した一連のルーティングが追加されていることがわかります。
ちなみに現状だとコントローラーにはそれぞれ対応するメソッドは用意されているものの、中身自体は空なので、最小限のコードを追加して表現するとこのようになります。
登録・編集画面などのビューはまだ作成していませんので、あくまでイメージですが、APIとして運用するならほとんどCRUD処理自体は完成していると言えます。
PostController.php
namespace App\Http\Controllers;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Models\Product;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class ProductController extends Controller
{
/**
* 一覧表示
*/
public function index(): View
{
$products = Product::all();
return view('products.index', compact('products'));
}
/**
* 作成フォーム表示
*/
public function create(): View
{
return view('products.create');
}
/**
* 新規作成処理
*/
public function store(StoreProductRequest $request): RedirectResponse
{
Product::create($request->validated());
return redirect()->route('products.index');
}
/**
* 詳細表示
*/
public function show(Product $product): View
{
return view('products.show', compact('product'));
}
/**
* 編集フォーム表示
*/
public function edit(Product $product): View
{
return view('products.edit', compact('product'));
}
/**
* 更新処理
*/
public function update(UpdateProductRequest $request, Product $product): RedirectResponse
{
$product->update($request->validated());
return redirect()->route('products.show', $product);
}
/**
* 削除処理
*/
public function destroy(Product $product): RedirectResponse
{
$product->delete();
return redirect()->route('products.index');
}
}
ここで、例えばshowアクションに注目してみると、メソッドの引数としてProductモデルが渡されてきていることに気づきます。
public function show(Product $product): View
{
return view('products.show', compact('product'));
}
showアクションのルーティングは products/{id}
なのですが、ここでパスに指定されているidをLaravelが自動で検知して、対応するProductモデルがshowメソッドの引数として注入されてきます。
これもEloquentモデルが基底に存在する前提を利用した強力な機能になっています。
最初の自動生成によって追加されたファイルはまだまだたくさんあるのですが、全ての機能に触れていると情報量が多すぎてしまうので一旦は
- モデル起点でベースの機能群が一気に追加される
- Eloquent ORMを利用した記述+付随する前提機能によりCRUD処理が数行で完成
の威力を感じていただければと思います。
注意しないといけないポイント
上記で紹介した利点はLaravelを使っていく上でふんだんに利用していくべきポイントである一方で、以下を念頭に置いて使用していくことが重要です。
1リソース=1モデルの前提があることを明確に意識
利点の方でも触れているのですが、改めて強調しておきたい部分です。Laravelは初心者から熟練者まで多くのユーザーを対象にしていることから幅広い表現を許容しています。そのためか、プロダクトごとにさまざまな書き方が行われていて
Laravelの特徴なのか、そのプロダクト特有の事情なのかが入り乱れがちだと感じています。
LaravelのベースはEloquent ORMを前提としたリソース志向のフレームワークであることを意識した上で、各プロダクトの事情ごとにどのようにLaravelと向き合っていくべきなのかを考えることが重要だと思います。
参考として、上記の説明をより詳しく解説しているこちらのスライドが本当におすすめです(Youtubeに動画もあります)
ビジネスロジックが複雑化してくるとモデルの責務が大きくなりすぎる(Fat Model)
1点目でも触れましたが、アプリケーションが複雑化してくることで最初のシンプルな書き方で対応するのが難しくなり、そのためプロダクトごとにさまざまな手段で複雑化と向き合っていく必要が出てきます。
Laravelでいうと大抵モデルに記述するロジックが複雑化してくることが多く、その責務を分割するための対策を講じることがよくあると思います。
少し具体例を見てみましょう。
以下は最初シンプルだったProductモデルに徐々にロジックが増え始め、さまざまな責務を背負うことになる状況のイメージです。
Product.php
// 最初はシンプルなデータベースのラッパー
class Product extends Model
{
protected $fillable = ['name', 'price'];
}
↓
Product.php
// 表示可否や価格の計算などの業務ロジックが増えてくる
class Product extends Model
{
protected $fillable = ['name', 'price', 'weight', 'is_hidden', 'stock'];
public function isInStock(): bool
{
return $this->stock > 0;
}
public function applyDiscount(int $percent): void
{
$this->price = floor($this->price * (1 - $percent / 100));
}
public function shippingCost(): int
{
return $this->weight > 1000 ? 800 : 500;
}
}
↓
Product.php
class Product extends Model
{
// データ定義
protected $fillable = [...];
// ビジネスルール
public function isInStock(): bool { ... }
public function applyDiscount(int $percent): void { ... }
public function shippingCost(): int { ... }
public function isVisible(): bool { ... }
// 表示関連ロジック
public function displayName(): string
{
return $this->is_hidden ? '[非公開] ' . $this->name : $this->name;
}
// データ整形ロジック
public function toApiFormat(): array
{
return [
'name' => $this->name,
'price' => $this->price . '円',
'shipping' => $this->shippingCost() . '円',
];
}
// 他のモデルとの結合ロジック
public function relatedCampaignDiscount(): int
{
return optional($this->campaign)->discount_percent ?? 0;
}
// ...まだまだ増えそう
}
どんどんメソッドが増えていっていますが、これらのメソッドは同時に使われることなくさまざまな背景の機能群が1ファイルに集約されていき、いわゆるFat Modelという状態になります。
ここまでくると大抵はサービスクラスなどに各処理を分割していくなどの解決策が取られることになります。
こういった背景を理解した上で業務のコードベースを見ていくと、どうして独自の解決策が取られているのか、Laravelの基本機能と乖離しているのかを理解することにもつながるかもしれません。
参考として(めちゃくちゃ有名な記事ですが)、具体的な実装イメージについてはこちらが参考になります。
Laravelは広く使われているフレームワークであるからこそ「とりあえず便利だから使っておこう」となりがちかもしれませんが、その特性・得意不得意を知っておくことで
- 使える場面では積極的にLaravelの敷いた道に乗っかる
- 使えない場面ではある程度割り切ってLaravelから外れた方法をとる
といった判断をする際に迷わなくなると思いますし、Laravelはそうした使い分けができるよう設計されていると思います。
本来はその関連でサービスコンテナについても触れたかったのですが
長くなってきそうだったので別記事にしたいと思います。
Views: 0