していること
ララベルで実装されたECサイトのリーディングをしていきたいと思います。
使用するオープンソースのURLはこちらです。
https://github.com/jsdecena/laracom
見た目を確認しておく
商品を検索してみます。
http://localhost:8000/search?q=accusamus
accusamusで検索をすると、URLクエリパラメタqにaccusamusが代入され、accusamusを含む商品のみが表示されます。
見えないところで起きていること
- ルーティングの定義
- リクエストの送信
- ルーティングのマッチング
- ミドルウェアの処理(今回は関連づけられていません)
- コントローラーのアクションの実行
- レスポンスの生成と送信
1つ1つ具体的に掘り下げていきます。
1. ルーティングの定義
これは我々が記述するルーティングのことです。web.phpファイルには、ルーティングを定義します。ルーティングは、特定のURLパスに対してどのコントローラーのアクションを実行するかを指定するものです。実際に、このECサイトの商品検索ページについて、このようなルーティングの記述があります。
Route::get("search", 'ProductController@search')->name('search.product');
ルートパス「search」というgetリクエストがあったとき、'ProductController'の'search'メソッドを実行するよう定義されています。
2.リクエストの送信
クライアント側から特定のURLに対してHTTPリクエストが送信されます。ブラウザから'search'へのGETリクエストが送信されます。
3. ルーティングのマッチング
Laravelのルーターが、送信されたリクエストのルートパスと定義されたルーティングを照合します。ルートパスと一致するルーティングが見つかった場合、そのルートに関連付けられたアクションが実行される準備が整います。
4 ミドルウェアの処理
今回は割愛します。'auth','web'ミドルウェアが適用されているページがあるため、それはその時に言及します。
5. コントローラーのアクションの実行
ルートに関連付けられたコントローラーのアクションが実行されます。
どのようにしてProductControllerを見つけるかを厳密に追います。
そもそもルーティングの定義で触れたルーティングの記述は、このような記述に囲まれています。
Route::namespace('Front')->group(function () {
...
Route::get("search", 'ProductController@search')->name('search.product');
...
}
一行目に関して、'Front'という指定した名前空間(namespace)内にあるコントローラーに対して、グループ内のすべてのルートが適用されるようになります。ルーティングのグループ化です。
Laravelの名前空間の指定するには、ファイルの先頭に名前空間を指定する必要があります。実際にProductController.phpの先頭にはこのような記述があり、名前空間を宣言しています。
<?php
namespace App\Http\Controllers\Front;
//以下Frontという名前空間
しかし、Laravelにおいて、名前空間とディレクトリ構成を一致させる必要はありませんが、一致させることが推奨されます。あくまで推奨であり、Laravel側はディレクトリ構成は参照せずに、名前空間から探します。 私はこのことを知らなかったため、名前空間の概念の理解に時間がかかりました。このサンプルコードは名前空間とディレクトリ構成が一致しているため(私が調べた範囲では)、名前空間=ディレクトリ構成という認識で話を進めていきます。なお、名前空間自体の理解に関しては、以下のサイトを参考にしてください。
コントローラーファイルは、大きく3種類に分かれており、Admin(管理社)、Auth(認証に関するもの)、Front(利用者)の3種類に大きく分かれています。
名前空間=ディレクトリですので、これら3種類のディレクトリがあります。
Laravelでは、コントローラーのクラス名とファイル名を一致させる必要はありませんが、一致させることが推奨されています。 これも同様に知りませんでした。今回実行するのはProductControllerですので、クラス名とファイル名が一致していることを信じて、ProductController.phpを開きます。
<?php
namespace App\Http\Controllers\Front;
...
class ProductController extends Controller
{
...
));
}
}
確かに、Frontという名前空間にあるProductControllerが見つかりました。呼び出されたProductControllerはインスタンス化されます。インスタンス化された時、まず初めに__constrctが呼び出されます。ProductRepositoryInterfaceの実装クラスのインスタンスが注入されます。コンストラクタ??インターフェース??といった疑問が生まれると思うため、一旦飛ばします。次回に詳しく追いたいと思います。とりあえず、ProductControllerのsearchメソッドが実行されるようです。メソッドによって返されるデータとその後の流れだけ追います。今回の記事では雰囲気だけ掴み取ってください。
public function search()
{
// request()はララベルのヘルパ関数
// 引数を空にすると「Illuminate\Http\Request」のインスタンスが返ってくる
$list = $this->productRepo->searchProduct(request()->input('q'));
// map:配列それぞれに関数を通す
$products = $list->where('status', 1)->map(function (Product $item) {
// transformProduct関数
// 商品それぞれの情報をインスタンスにまとめる関数
return $this->transformProduct($item);
});
// 10個ずつ返す
return view('front.products.product-search', [
'products' => $this->productRepo->paginateArrayResults($products->all(), 10)
]);
}
$listに検索された商品が代入されます。$productsに$listを色々こねてから代入します。$listを「status=1(削除された商品でないこと)」で絞り込み、map関数で一つ一つの商品を整形をし、返り値をそのまま$productsに代入(アロー関数) をしています。
front.products.product-searchにビューを返します。フロントで表示されるサイトです。それと同時に、productsに諸々整形したデータ($product)を持たせます。これはビューファイルで$productsとして扱えます。ちなみに、paginateArrayResults()はページネーション(10個ずつ表示など)のために用意されたヘルパ関数のはずです。
6. レスポンスの生成と送信
アクションが実行された後、結果としてレスポンスが生成されます。レスポンスは、クライアントに返されるデータやHTTPステータスコードなどの情報を含みます。例えば、アクションがビューを返す場合、そのビューがレンダリングされてHTMLコンテンツとしてレスポンスに含まれます。最終的なレスポンスが生成されたら、それがHTTPレスポンスとしてクライアントに送信されます。クライアントは、受け取ったレスポンスを処理し、表示または別の操作を行います。
ビューはどんなファイルなのでしょうか。コントローラーが返したビューファイルを見てみます。
@extends('layouts.front.app')
@section('content')
<div class="container">
<hr>
<div class="row">
<div class="category-top col-md-12">
<h2>Search Results</h2>
</div>
</div>
<hr>
<div class="col-md-3">
@include('front.categories.sidebar-category')
</div>
<div class="col-md-9">
<div class="row">
@include('front.products.product-list', ['products' => $products])
</div>
</div>
</div>
@endsection
共通部分は別のファイルlayouts.front.appに切り分けてあります。これについてもいつか触れるかもしれません。商品の陳列部分だけ、といいたいところですが、商品がforeachやらで回される部分はさらに他のページに切り分けられていますね。front.products.product-listを参照します。(layouts.front.category-sidebar-subの方は一回触れないでおきます)
@if (!empty($products) && !collect($products)->isEmpty())
<ul class="row text-center list-unstyled">
@foreach ($products as $product)
<li class="col-md-3 col-sm-6 col-xs-12 product-list">
//(中略)
<h4>{{ $product->name }}</h4>
//(中略)
</li>
@endforeach
@if ($products instanceof \Illuminate\Contracts\Pagination\LengthAwarePaginator)
<div class="row">
<div class="col-md-12">
<div class="pull-left">{{ $products->links() }}</div>
</div>
</div>
@endif
</ul>
@else
<p class="alert alert-warning">No products yet.</p>
@endif
しっかりforeachが出てきましたね。省略した部分で$product->nameなどといった形で渡された値がやっと使われます。$productsが空っぽだった場合は条件分岐させることによりNo products yet.が表示されるようにしてあるのもバッチリです。
あとがき
ドラーマーク($)のエスケープがうまくいかず、大文字のドラーマークで代用した部分があります。どういう仕様なのかわかりませんが、詳細わかる方教えてください(;;)