はじめに
この記事では、LaravelとVue.jsを使用して、CRUDを実装し商品管理システム作成する方法を紹介します。内容はGPT4と会話形式で進めながら作成したものになるため情報源は2021年9月までの情報になります
必要なもの
- PHP 7.3 以上
- Composer
- Laravel 8.x 以上
プロジェクトの作成
新しいLaravelのプロジェクトはコマンドで作成します。そのためにまずはプロジェクトの置き場所にカレントディレクトリ(CD)を移動させます
cd プロジェクトを作成したいディレクトリ(フォルダ)のパス
プロジェクト名はproduct_managementとしていますが、任意の名前に置き換えて下さい
composer create-project --prefer-dist laravel/laravel product_management
次に、作成したプロジェクトのディレクトリに移動します。
cd product_management
コマンドについての補足
- composer
PHPのプログラムを書くときに役立つツールです。プログラムが必要とするパーツ(パッケージ)をダウンロードしたり、必要なバージョンを管理したりすることができます。composerがPCにインストールされていない場合はエラーが発生するため、インストールも必要です - create-project
composerの「新しいプロジェクトを作ってください」という命令です。 - --prefer-dist
「可能であれば、パッケージの完成形をダウンロードしてください」という追加の指示です。これにより、ダウンロードが早くなったり、必要なデータが少なくなったりします。 - laravel/laravel
ダウンロードしたいパッケージの名前です。これは「Laravel」というフレームワークを指します - product_management
作成する新しいプロジェクトの名前です。
【補足】バージョンを指定したい場合
バージョンを指定してプロジェクトを作成する場合は、以下のようにパッケージ名の後にコロン(:)とバージョン番号を付けて実行します。
composer create-project --prefer-dist laravel/laravel:8.0 product_management
これは、「Laravel」の「8.0」バージョンを使って新しいプロジェクトを作る、という意味です。ただし、バージョンを指定する必要がなければ、最初のコマンドのようにバージョン番号を省略することもできます。その場合、自動的に最新バージョンがダウンロードされます。
バージョンを指定する際に8.*と記述すると、8のメジャーバージョンの中で最新のマイナーバージョンがインストールされます。例えば、Laravelの最新バージョンが8.12.3だとすると、8.*と指定することで8.12.3がインストールされます。
そのため、以下のようにコマンドを実行すると、Laravel 8の最新バージョンを使用したプロジェクトを作成することができます。
composer create-project --prefer-dist laravel/laravel:8.* product_management
Laravel UIのインストール
Laravel UIは、Laravelフレームワークのための公式フロントエンドパッケージで、ユーザー認証(ログインと登録)のためのビューとコントローラを簡単に生成することができます。
認証機能を作成するほかにも、BootstrapやVue、Reactなどのフレームワークを組み込む機能も含まれています。これにより、見た目の美しいWebページを短時間で作成することが可能になります。
Laravelプロジェクトを作成しただけでは、これらの認証機能やフレームワークの組み込みは提供されません。そのため、このような機能が必要な場合は、Laravel UIをインストールする必要があります。
補足として、現在ではより2段階認証などの強力なセキュリティ機能を含んだJetstream
やFortify
といったパッケージが認証機能の提供のために推奨されていますが、その使い方は少し複雑で難しいため、今回の記事ではLaravel UIを紹介しています
composer require laravel/ui
データベースを構築する
商品管理システムなので商品データを保存するデータベースが必要です。
データベースを準備する
データベースの構築には専用のアプリが必要で有名なものでは以下のようなものがあります
- MySQL
- Oracle Database
- SQLite
- SQLLite
- IBM DB2
いずれかのデータベースソフトを起動し、その中にデータベースとテーブルを作成する必要です。
著者の環境ではMac PCを使用しているため、インストールするだけでMySQL
を使用できるMamp
というソフトを使用できる状態にしてあるので、データベースソフトのインストールについても割愛します
データベースと接続する(MySQLの場合)
起動しているデータベースに接続するには以下のような情報が必要です
これはMampというソフトから公開されているMySQLの接続情報になります。これを使ってローカルのMySQLとLaravelのプロジェクトを接続させ、データベースとテーブルを作成します
使用するデータベースによって設定方法が違いますが、MySQL以外の接続方法については割愛しています
envファイルの編集(接続情報の入力)
作成したLaravelプロジェクトの直下に.env
という隠しファイルが存在します
envファイルにデータベースの接続情報を保存することで、対象のデータベースに自動で接続されます
隠しファイルの表示方法
-
Windowsの場合
タスク バーから エクスプローラー を開きます。[表示] > [表示] > [隠しファイル] を選択します。
-
Macの場合
「command」+「shift」+「.」 を同時押します
.envファイルの編集(MySQLの場合)
.envに次の項目が含まれているので接続情報を保存します
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=8889
DB_DATABASE=mydatabase
DB_USERNAME=root
DB_PASSWORD=root
DB_SOCKET=/Applications/MAMP/tmp/mysql/mysql.sock
-
DB_HOST
データベースがどこにあるかを指定します。今回は自分のパソコンにあるので127.0.0.1
を指定します。 -
DB_PORT
データベースに接続するためのポート番号を指定します。
MAMPのデフォルトは「8889」なのでそれを指定します。 -
DB_DATABASE
データベースの名前を指定します。
任意の名前を設定しますが今回は「mydatabase」を指定します -
DB_USERNAMEとDB_PASSWORD
データベースに接続するためのユーザー名とパスワードを指定します
MAMPのデフォルトは「root」なのでそれを指定します。 -
DB_SOCKET
MAMPを使っている場合に必要になります
MAMPのMySQLのソケットパスを指定します。
MySQLではない場合、他の項目に情報を入力する場合があります
データベースの接続確認
次のマイグレーションコマンドはLaravelに必要なテーブルを作成するコマンドですが、実行した際にエラーが出ずコマンドが正常に完了すれば、データベースへの接続は成功を意味します。
php artisan migrate
その際に接続したデータベースソフトに該当するデータベース名(今回はmydatabase)が存在しない場合は次のメッセージが表示されます
WARN The database 'mydatabase' does not exist on the 'mysql' connection.
┌ Would you like to create it? ────────────────────────────────┐
│ ○ Yes / ● No │
yesを選択することで接続するMySQLにmydatabase
というデータベースが作成されることになります。
Laravelのバージョンや接続するデータベースによっては自動作成ができずエラーで止まってしまう場合があるのでその際は次の補足を参考に、ターミナルやコマンドプロンプトからデータベースに接続し、データベースを作成する必要があります
(補足)コマンドでデータベースと接続する
無事データベースとの接続が成功した場合(マイグレーションが成功した場合)はスキップして下さい
- MampのMySQLと接続する場合の例
mysql -u root -p -S /Applications/MAMP/tmp/mysql/mysql.sock
- データベースを作成する例
CREATE DATABASE mydatabase;
- SQL画面を終了する
exit
マイグレーションの実行
接続済みのデータベースにテーブルを作成するためマイグレーション
を行います。これによりユーザー情報を保存するための users テーブルが作成されます。以下のコマンドを実行します。
php artisan migrate
この時データベースとの接続情報が不十分だった場合は接続ができずエラーが発生します。エラーが出た場合は入力ミスなどがないか、もしくはデータベースソフトが起動しているか確認します
ログイン機能を作成する
認証ビューの生成
Laravelでは以下のコマンドで認証に関連するファイル(登録、ログイン、パスワードリセットなど)を自動生成できます。
php artisan ui vue --auth
これでLaravelが必要とするすべての認証ビューが生成されますが、次のような警告メッセージも表示されたと思います
Please run [npm install && npm run dev] to compile your fresh scaffolding.
新しく作成されたファイルは、ブラウザがそのまま理解できる形式ではなく変換作業が必要になるため、この変換作業を行うためにはnpm install
とnpm run dev
というコマンドを実行する必要があることを警告しています
viewの表示確認
ここで試しにviewが閲覧できるか確認します
php artisan serve
// サーバー機能を終了する場合は Ctrl + c です
カレントディレクトリをプロジェクトのルートディレクトリに設定した状態で上記のコマンドを実行するとPHPのビルトインサーバーを起動してLaravelアプリケーションを動かすことができます
つまりLaravelのウェブサイトを自分のコンピューター上で見ることができるようにします。
サーバーを起動したまま、次のアドレスにアクセスすることで、プロジェクトのWEBサイトを閲覧することができます
もしくは
検索すると次の画面になるかと思います
これはLaravelのプロジェクトに元々存在するviewのファイルになりますが、右上のLog in
とRegister
というリンクは--auth
コマンドにて用意されたリンク先なので開こうとしても次のように閲覧がまだできません
npmインストールについて
「npm」は、JavaScriptのプログラムを作る際に使うツールの一つです。
php artisan ui vue --auth
というコマンドはJavaScriptのライブラリを使ったユーザー認証システムなのでJavaScriptのライブラリを必要としています
npmはJavaScriptのプログラムを作るための「材料」を提供してくれる場所、つまり「スーパーマーケット」のようなものなので、まずはnpmを使えるようにインストールします
npm install
npm run devについて
これは開発者が自分のコンピュータ上でプロジェクトを実行・テストするためのコマンドです
ウェブサイトを動かすためのJavaScriptやCSSなどのコードを、ブラウザが理解できる形に「組み立て」ます。この組み立て作業を「ビルド」と呼びます。ここでは、開発者が書いた原始的なコードを、ブラウザが理解できるように翻訳(トランスパイル)したり、複数のファイルを一つにまとめたり(バンドル)します。
つまり、npm run devは、--auth
で書かれたコードをビルドして、それを自分のパソコンで確認できるようにするためのコマンドということになります。
npm run dev
// 終了する場合は Ctrl + c
ログイン画面の確認
もう一度確認します
もしくは
今度はログイン画面が閲覧できるようになるはずです
【補足】npm run buildについて
npm run dev
では機能を停止するとWEBページを閲覧ができなくなるためnpm run build
を実行することでnpm run dev
を起動しなくても閲覧が可能になります
npm run build
これは本番環境用としてpackage.jsonとpackage-lock.jsonというファイルが配置され、npm run dev
をしなくともログインページが閲覧できるようにするコマンドです
テーブルを実装する
ログインに必要なusersテーブル
はこれまでの手順(コマンド)で作成されていますが、商品管理をするためにこれから次のテーブルを実装します
- products(商品管理テーブル)
- sales(売上管理テーブル)
- companies(メーカー管理テーブル)
products テーブル(商品管理)
Column Name | Type |
---|---|
id | Primary Key |
company_id | Foreign Key |
product_name | string |
price | decimal |
stock | integer |
comment | text |
img_path | string |
created_at | timestamp |
updated_at | timestamp |
sales テーブル(売上管理)
Column Name | Type |
---|---|
id | Primary Key |
product_id | Foreign Key |
created_at | timestamp |
updated_at | timestamp |
companies テーブル(メーカー管理)
Column Name | Type |
---|---|
id | Primary Key |
company_name | string |
street_address | string |
representative_name | string |
created_at | timestamp |
updated_at | timestamp |
テーブルを作成する(マイグレーションファイルの作成)
データベースのテーブルをSQLコマンドで一つ一つ作成するのは手間がかかります。そこでLaravelではテーブルのカラム(列)の名称やデータ型をまとめた設計図、マイグレーションファイル
を用意しマイグレーションコマンド
を実行することで一括でテーブルを作成します
LaravelはMVC構造を採用しているため、データベースのテーブルを操作するためにはモデルファイル
が必要です。そこでマイグレーションファイルとモデルファイルの両方を次のコマンドで作成していきます
マイグレーションファイルの作り方(同時にモデルファイルも作成)
- productsのマイグレーションファイルと Product モデルの作成
php artisan make:model Product -m
- companiesマイグレーションファイルと Company モデルの作成
php artisan make:model Company -m
- salesマイグレーションファイルと Sale モデルの作成
php artisan make:model Sale -m
上記の-m オプション
がマイグレーションファイルを追加で作成するためのオプションです。マイグレーションファイルの名前も自動で命名してくれるので非常に便利です
【補足】マイグレーションファイルを単体で作成する
php artisan make:migration create_[テーブル名]_table
マイグレーションファイルの名前は、現在のタイムスタンプと指定したテーブル名を組み合わせたものになります。
ファイルの置き場所
laravel-project-root-directory
│
├── app
│ ├── ...
│ ├── Http
│ ├── Providers
│ └── Models <- モデルファイルはここに配置されます。
│ └── Product.php
│
├── bootstrap
│
├── config
│
├── database
│ ├── factories
│ ├── seeds
│ └── migrations <- マイグレーションファイルはここに配置されます。
│ ├── 2021_01_01_000000_create_products_table.php
│ └── ...
│
└── ...
マイグレーションファイルの書き方について
マイグレーションファイルはデータベースのテーブルを作成・変更するための指示書です
マイグレーションファイルの構造
マイグレーションファイルにはup() と down()というメソッドが2つ存在します。
-
up()
テーブルの作成やカラムの追加など、データベースに変更を加えるためのコードを書きます。 -
down()
up() で行った変更を元に戻すためのコードを書きます。例えば、up() でテーブルを作成した場合、down() でそのテーブルを削除するコードを書きます。
【補足】テーブルの作成
テーブルを作成する際には、Schema::create メソッドを使用します。
Schema::create('テーブル名', function (Blueprint $table) {
$table->id(); // IDカラム
$table->string('name'); // 文字列カラム
// その他のカラムをこちらに追加
$table->timestamps(); // 作成日時と更新日時カラム
});
【補足】テーブルの変更
既存のテーブルを変更する場合、Schema::table メソッドを使用します。
Schema::table('テーブル名', function (Blueprint $table) {
$table->string('新しいカラム名');
});
【補足】テーブルの削除
テーブルを削除するには、Schema::dropIfExists メソッドを使用します。
Schema::dropIfExists('テーブル名');
このように、マイグレーションファイルを使ってデータベースのテーブルの作成や変更を行うことができます。マイグレーションが完成したらphp artisan migrate
コマンドを使って、実際にデータベースに反映させます。
productsテーブルのマイグレーション
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProductsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id(); // bigint(20) の ID
$table->unsignedBigInteger('company_id');
$table->string('product_name', 255);
$table->integer('price'); // int(11)
$table->integer('stock'); // int(11)
$table->text('comment')->nullable();
$table->string('img_path', 255)->nullable();
$table->timestamps(); // created_at と updated_at の timestamp
// 外部キー制約を追加
$table->foreign('company_id')->references('id')->on('companies');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('products');
}
}
companies(メーカー管理テーブル)のコード
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCompaniesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('companies', function (Blueprint $table) {
$table->id(); // bigint(20) の ID
$table->string('company_name', 255);
$table->string('street_address', 255);
$table->string('representative_name', 255);
$table->timestamps(); // created_at と updated_at の timestamp
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('companies');
}
}
salesテーブルのマイグレーション
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSalesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('sales', function (Blueprint $table) {
$table->id(); // bigint(20) の ID
$table->unsignedBigInteger('product_id');
$table->timestamps(); // created_at と updated_at の timestamp
// 外部キー制約を追加
$table->foreign('product_id')->references('id')->on('products');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('sales');
}
}
マイグレーションの実行
接続済みのデータベースにテーブルを作成するためマイグレーション
を行います。
php artisan migrate
一度実行したマイグレーションファイルは使用済みになり、ファイル名を変更しない限り再度実行されることはありません
まれにテーブルが作成されたにも関わらず使用済みにならない場合があります。再度マイグレーションでテーブルを作ろうとすると重複エラーになりマイグレーションが中断されます。その際は使用済みのマイグレーションファイルは他の場所に一時移動させるなどの対処が必要です
【補足】外部キーの制約について
外部キーの制約を追加するとは、あるテーブルの中のあるカラム(情報の欄)が、別のテーブルの特定のカラムと関連していることを示す設定のことを指します。
例として私たちが「商品」テーブルと「会社」テーブルを持っている場合では
「商品」テーブルには「会社ID」という欄があり、この欄はどの会社がその商品を製造または販売しているのかを示しています。しかし、この「会社ID」だけを見ても、その会社が具体的に何かわかりません。それを知るためには「会社」テーブルを参照する必要があります。
外部キーの制約を設定すると、「商品」テーブルの「会社ID」が「会社」テーブルの「ID」と一致していることをシステムに伝えることができます。これにより、間違って存在しない「会社ID」を「商品」テーブルに追加することを防ぐことができます。
まとめると、外部キーの制約は「この情報は、もう一つのテーブルの特定の情報とリンクしているよ」とシステムに教える役割を果たしています。これにより、データの整合性を保つのを助け、間違ったデータの追加を防ぎます。
外部キーの制約を追加しない場合は「商品」テーブルに存在しない「会社ID」を追加することが可能となります。これにより、データの整合性が失われる恐れがあります。
リレーション関係(外部キー)について
それぞれのテーブルは以下のようなリレーションの関係になっております
- products(商品)が1に対してsales(売上記録)は多
- products(商品)が多に対してcompanies(メーカー名)は1
companies (1) ---* products (1) ---* sales
リレーションとは異なるテーブルが関連付けされた関係を指し、米マークは多
を示しています
例えば特定の商品の詳細ページ
を開く場合、主な情報はproducts
から参照できますが、商品
のメーカー名
についてはcompanies
に記載されているため商品に紐づいたメーカー名
を参照する場合は事前にリレーション設定が必要になります
【補足】外部キーについて
テーブルとテーブルを紐付けするためには外部キー
が必要です
外部キーの命名規則
外部キーは"単数形のテーブル名_id"とするのが慣例です。たとえば、usersテーブルへの外部キーはuser_id
、productsテーブルへの外部キーはproduct_id
となります。
データ型
外部キーのデータ型は、関連する主キー(id)のデータ型と一致させる必要があります。一般的には、主キー(id)は符号なしの整数(unsigned integer)として定義されます。そのため、外部キーも同様に符号なしの整数として定義するのが一般的です。
外部キーが必要なテーブル
外部キーは、あるテーブルのカラムが別のテーブルの特定のカラム(通常は主キー)を参照して関連付けるために使用されるキーです。したがって、どのテーブルに外部キーが必要かは、テーブル間の関係やデータの整合性をどのように保つかによって決まります。
- 今回の場合は、以下のような関係がありました
products テーブルは、商品がどの会社に所属しているかを示すために company_id カラムを持っています。この company_id は、companies テーブルの id カラムを参照する外部キーとして設定されるべきです。
同様に、sales テーブルは、どの商品が売れたのかを示すために product_id カラムを持っています。この product_id は、products テーブルの id カラムを参照する外部キーとして設定されるべきです。
外部キーを設置するテーブル
テーブルの外部キーは1対多、多対1などの場合、多
のテーブルにだけ設置します。このような構造にすることで、例えば特定の会社のすべての商品を取得したい場合、商品テーブル(products)でその会社のID(company_id)を検索条件として使用することができます。
リレーション設定について(モデル)
Laravelでテーブル同士にリレーション関係を設定するためには以下の2つが必要です
- データベースのテーブルに外部キーを設置する
- モデルにリレーション関係を示すメソッド(コード)を記述する
テーブルの作成段階で外部キーの設置は済んでいるので、モデルに対してリレーションを示すメソッドが必要です
リレーションを示すメソッド(モデル)
/(ルートディレクトリ)
|
└── app/(appディレクトリ)
|
└── Models/(Modelsディレクトリ)
|
└── Product.php(Product モデル)
<?php
namespace App\Models;
// 使うツールを取り込んでいます。
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
// Productという名前のツール(クラス)を作っています。
class Product extends Model
{
// ダミーレコードを代入する機能を使うことを宣言しています。
use HasFactory;
// 以下の情報(属性)を一度に保存したり変更したりできるように設定しています。
// $fillable を設定しないと、Laravelはセキュリティリスクを避けるために、この一括代入をブロックします。
protected $fillable = [
'product_name',
'price',
'stock',
'company_id',
'comment',
'img_path',
];
// Productモデルがsalesテーブルとリレーション関係を結ぶためのメソッドです
public function sales()
{
return $this->hasMany(Sale::class);
}
// Productモデルがcompanysテーブルとリレーション関係を結ぶ為のメソッドです
public function company()
{
return $this->belongsTo(Company::class);
}
}
<?php
namespace App\Models;
// 使うツールを取り込んでいます。
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
// Companyという名前のツール(クラス)を作っています。
class Company extends Model
{
use HasFactory;
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Sale extends Model
{
use HasFactory;
public function product()
{
return $this->belongsTo(Product::class);
}
}
【補足】用語解説
- namespace App\Models;
名前空間を定義しており、このファイルがApp\Modelsという場所に存在することをLaravel(php)に伝えています
-
use文 (例: use Illuminate\Database\Eloquent\Model;)
外部のツールや機能を使うための宣言。これを使うことでそのツールを簡単に使えるようになります。今回はリレーション関係のため他のモデルファイルを参照することと、ランダムでダミーデータを作成するHasFactoryを使うためにuse
を用いています -
class Sale extends Model
Sale
という名前の新しいツール(クラス)を作成しています。そして、Modelという基本的なツールを基にして、Saleのツールを作ります。
-
use HasFactory;
テーブルに対してダミーデータをテスト用に簡単に作ることができます。 -
protected $fillable = [ ... ];
モデルのデータ(例えば、商品の情報)をデータベースに保存する際どの情報を編集や追加することができるかを設定する部分です。
ここで指定されている項目('product_name', 'price', ...)は、データベースに追加や更新を許可されている情報です。
外部からの不正な情報の変更や追加を防ぐためのセキュリティ機能として利用されます。指定されていない情報はデータベースに直接追加や更新することができません。
これらのコードはデータの扱いや関連付けをより柔軟で安全に行うためのものです。初心者がこれらの機能を理解して使えるようになると、データ操作の効率やセキュリティが大幅に向上します。
hasManyとbelongsToの書き方
2つのテーブルをリレーションで関連付けするためにはそれぞれにリレーションのメソッドを追加する必要があります
hasManyとbelongsToはそれぞれ対となるリレーション関係を示します。どちらをどのモデルに書くかは、そのモデルがリレーションにおいて"1"の側であるか、"多"の側であるかによって決まります。
hasManyは「一つのモデルが他の多くのモデルを所有している」関係を表すので、「1」の側のモデルに書きます。例えば、一つのメーカー(Company)が多くの商品(Product)を製造している場合、メーカーモデルにhasManyメソッドを使って以下のように書くことで、そのメーカーが製造したすべての商品を取得することができます。
public function products()
{
return $this->hasMany(Product::class);
}
belongsToは「一つのモデルが他の一つのモデルに所属している」関係を表すので、「多」の側のモデルに書きます。例えば、多くの商品(Product)がそれぞれ一つのメーカー(Company)に属している場合、商品モデルにbelongsToメソッドを使って以下のように書くことで、その商品のメーカーを取得することができます。
public function company()
{
return $this->belongsTo(Company::class);
}
したがって、どちらのメソッドをどのモデルに書くかは、そのモデルが1対多のリレーションにおいて「1」の側であるか、「多」の側であるかによります。
ファイル名とメソッド名について
メソッド名は通常、リレーション先のモデル名(単数形または複数形)を使用します。そして、hasManyやbelongsToの引数には、リレーション先のモデルクラスを指定します。これにより、Laravelはどのモデルとリレーションを結ぶべきかを理解します。
hasManyの場合メソッド名は複数形のモデル名を使用します。これは、「一つのインスタンスが多くの他のインスタンスを所有している」ことを表しています。
CompanyモデルがProductモデルと1対多のリレーションを持つ場合、次のように書きます。
public function products()
{
return $this->hasMany(Product::class);
}
belongsToの場合メソッド名は単数形のモデル名を使用します。これは、「一つのインスタンスが他の一つのインスタンスに属している」ことを表しています。例えばProductモデルがCompanyモデルと多対1のリレーションを持つ場合、次のように書きます。
public function company()
{
return $this->belongsTo(Company::class);
}
このようにメソッド名と引数を使ってリレーションを定義することで、Laravelは関連するモデル間のリレーションを自動的に管理できます。これにより、データベースからのデータ取得や操作をより簡単に行うことができます。
【補足】CRUDを実装する
CRUDとは、データを操作するための4つの基本的な操作を表す英語の頭文字を取ったものです。
- Create(作成)
新しいデータを作成します。例えば、新しいユーザーや新しい商品の情報など、新しくデータを追加することがこれに当たります。 - Read(読み取り)
既存のデータを読み取ります。データベースから情報を取り出して、それをウェブページに表示するなどの操作がこれに当たります。 - Update(更新)
既存のデータを更新します。例えば、ユーザーが自分のプロフィール情報を変更することや、商品の在庫数を更新することなどがこれに当たります。 - Delete(削除)
既存のデータを削除します。不要になったデータをデータベースから削除する操作がこれに当たります。
Laravelではコマンドで上記動作のテンプレートを作成することができるため、次の手順でそれを操作していきます
CRUDのコントローラーを実装する
これまでの手順でMVCのモデル部分が作成完了しました、次はコントローラーの作成に移ります。
Laravelでのコントローラの作成は主にartisanコマンドを利用します。
php artisan make:controller ProductController --resource
これによりapp/Http/Controllers/ProductController.phpというファイルが作成されます。
/ (ルート、プロジェクトディレクトリの最上位)
│
└── app
│
└── Http
│
└── Controllers
│
└── YourController.php (あなたのコントローラーファイル)
--resourceオプション
を使用することでCRUD(作成、読み取り、更新、削除)操作を行うメソッドスケルトン(ひな形)が生成されます。
- index: リソースのリストを表示する
- create: 新規リソースの作成フォームを表示する
- store: 新規リソースを保存する
- show: 指定リソースを表示する
- edit: 指定リソースの編集フォームを表示する
- update: 指定リソースを更新する
- destroy: 指定リソースを削除する
コントローラーはモデル(テーブル)を操作するためのプログラムを書くファイルです。
今回は3つのテーブルがあるのでそれぞれの操作が必要であれば3つのコントローラーが必要になりますが、Productモデルを操作すればリレーションで紐づいた情報が呼び出せるため、コントローラーは今回ProductController
のみとなります
<?php
// まずは必要なモジュールを読み込んでいます。今回はProductとCompanyの情報と、リクエストの情報が必要です。
namespace App\Http\Controllers;
use App\Models\Product; // Productモデルを現在のファイルで使用できるようにするための宣言です。
use App\Models\Company; // Companyモデルを現在のファイルで使用できるようにするための宣言です。
use Illuminate\Http\Request; // Requestクラスという機能を使えるように宣言します
// Requestクラスはブラウザに表示させるフォームから送信されたデータをコントローラのメソッドで引数として受け取ることができます。
class ProductController extends Controller //コントローラークラスを継承します(コントローラーの機能が使えるようになります)
{
public function index()
{
// 全ての商品情報を取得しています。これが商品一覧画面で使われます。
$products = Product::all();
//productsという名前は任意名です。何を格納しているのかわかりやすい名前を付けます
//Productはモデル名を指しています。どのテーブルを操作するか指定します
//::all();はデータベーステーブルの全てのデータを取得するためのメソッドです
//$productsにはProductテーブルの全てのデータが取得し格納されます
// 商品一覧画面を表示します。その際に、先ほど取得した全ての商品情報を画面に渡します。
return view('products.index', compact('products'));
// productsディレクトリのindex.blade.phpを表示させます
// compact('products')によって
// $productsという変数の内容が、ビューファイル側で利用できるようになります。
// ビューファイル内で$productsと書くことでその変数の中身にアクセスできます。
}
public function create()
{
// 商品作成画面で会社の情報が必要なので、全ての会社の情報を取得します。
$companies = Company::all();
// 商品作成画面を表示します。その際に、先ほど取得した全ての会社情報を画面に渡します。
return view('products.create', compact('companies'));
}
// 送られたデータをデータベースに保存するメソッドです
public function store(Request $request) // フォームから送られたデータを $requestに代入して引数として渡している
{
// リクエストされた情報を確認して、必要な情報が全て揃っているかチェックします。
// ->validate()メソッドは送信されたリクエストデータが
// 特定の条件を満たしていることを確認します。
$request->validate([
'product_name' => 'required', //requiredは必須という意味です
'company_id' => 'required',
'price' => 'required',
'stock' => 'required',
'comment' => 'nullable', //'nullable'はそのフィールドが未入力でもOKという意味です
'img_path' => 'nullable|image|max:2048',
]);
// '|'はパイプと呼ばれる記号で、バリデーションルールを複数指定するときに使います
// 'image'はそのフィールドが画像ファイルであることを指定するルールです
// max:2048'は最大2048KB(2メガバイト)までという意味です
// フォームが一部空欄のまま送信ボタンを押しても、フォームの画面にリダイレクトされ
// フォームの値が未入力である旨の警告メッセージが表示されます
// 新しく商品を作ります。そのための情報はリクエストから取得します。
$product = new Product([
'product_name' => $request->get('product_name'),
'company_id' => $request->get('company_id'),
'price' => $request->get('price'),
'stock' => $request->get('stock'),
'comment' => $request->get('comment'),
]);
//new Product([]) によって新しい「Product」(レコード)を作成しています。
//new を使うことで新しいインスタンスを作成することができます
// リクエストに画像が含まれている場合、その画像を保存します。
if($request->hasFile('img_path')){
$filename = $request->img_path->getClientOriginalName();
$filePath = $request->img_path->storeAs('products', $filename, 'public');
$product->img_path = '/storage/' . $filePath;
}
// $request->hasFile('img_path')は、ブラウザにアップロードされたファイルが存在しているかを確認
// getClientOriginalName()はアップロードしたファイル名を取得するメソッドです。
// storeAs('products', $filename, 'public')は
// アップロードされたファイルを特定の場所に特定の名前で保存するためのメソッドです
// 今回はstorage/app/publicにproducts" ディレクトリが作られ保存されます
//'products':これはファイルを保存するディレクトリ(フォルダ)の名前を示しています。
// この場合は 'products' という名前のディレクトリにファイルが保存されます。
//$filename:これは保存するファイルの名前を示しています。
// getClientOriginalName() メソッドで取得したオリジナルのファイル名がここに入ります。
// 'public' ファイルのアクセス権限を示しています。'public' は公開設定で、誰でもこのファイルにアクセスすることができるようになります。
// 作成したデータベースに新しいレコードとして保存します。
$product->save();
// 全ての処理が終わったら、商品一覧画面に戻ります。
return redirect('products');
}
public function show(Product $product)
//(Product $product) 指定されたIDで商品をデータベースから自動的に検索し、その結果を $product に割り当てます。
{
// 商品詳細画面を表示します。その際に、商品の詳細情報を画面に渡します。
return view('products.show', ['product' => $product]);
// ビューへproductという変数が使えるように値を渡している
// ['product' => $product]でビューでproductを使えるようにしている
// compact('products')と行うことは同じであるためどちらでも良い
}
public function edit(Product $product)
{
// 商品編集画面で会社の情報が必要なので、全ての会社の情報を取得します。
$companies = Company::all();
// 商品編集画面を表示します。その際に、商品の情報と会社の情報を画面に渡します。
return view('products.edit', compact('product', 'companies'));
}
public function update(Request $request, Product $product)
{
// リクエストされた情報を確認して、必要な情報が全て揃っているかチェックします。
$request->validate([
'product_name' => 'required',
'price' => 'required',
'stock' => 'required',
]);
//バリデーションによりフォームに未入力項目があればエラーメッセー発生させる(未入力です など)
// 商品の情報を更新します。
$product->product_name = $request->product_name;
//productモデルのproduct_nameをフォームから送られたproduct_nameの値に書き換える
$product->price = $request->price;
$product->stock = $request->stock;
// 更新した商品を保存します。
$product->save();
// モデルインスタンスである$productに対して行われた変更をデータベースに保存するためのメソッド(機能)です。
// 全ての処理が終わったら、商品一覧画面に戻ります。
return redirect()->route('products.index')
->with('success', 'Product updated successfully');
// ビュー画面にメッセージを代入した変数(success)を送ります
}
public function destroy(Product $product)
//(Product $product) 指定されたIDで商品をデータベースから自動的に検索し、その結果を $product に割り当てます。
{
// 商品を削除します。
$product->delete();
// 全ての処理が終わったら、商品一覧画面に戻ります。
return redirect('/products');
//URLの/productsを検索します
//products /がなくても検索できます
}
}
ルーティングを作成する
ルーティングとは、インターネットを通じてあなたのウェブサイトの特定の部分への「道案内」のことを指します。コントローラーを作成しましたがこれだけではWEBサイトを表示できません
たとえば、ウェブサイトに訪れた人がウェブブラウザのアドレスバーにwww.yoursite.com/products
と入力したとします。このとき、ルーティングの設定が「/products」へのアクセスがあったときは「商品一覧ページ」を表示するように設定されていれば、その人は商品一覧ページを見ることができます。
Laravelではルーティングの設定をroutes
フォルダの中にあるweb.php
ファイルで行うため次のようにルーティングを記述します
今回はWEBサイトへのルーティングを記述するので、web.phpにコードを記述しますが、APIを起動させるルーティングの場合はapi.phpにコードを記述します
/ (ルート、プロジェクトディレクトリの最上位)
│
└── routes
│
├── web.php (ウェブルーティングの設定ファイル)
│
└── api.php (APIルーティングの設定ファイル)
<?php
use Illuminate\Support\Facades\Route;
// "Route"というツールを使うために必要な部品を取り込んでいます。
use App\Http\Controllers\ProductController;
// ProductControllerに繋げるために取り込んでいます
use Illuminate\Support\Facades\Auth;
// "Auth"という部品を使うために取り込んでいます。この部品はユーザー認証(ログイン)に関する処理を行います
Route::get('/', function () {
// ウェブサイトのホームページ('/'のURL)にアクセスした場合のルートです
if (Auth::check()) {
// ログイン状態ならば
return redirect()->route('products.index');
// 商品一覧ページ(ProductControllerのindexメソッドが処理)へリダイレクトします
} else {
// ログイン状態でなければ
return redirect()->route('login');
// ログイン画面へリダイレクトします
}
});
// もしCompanyControllerだった場合は
// companies.index のように、英語の正しい複数形になります。
Auth::routes();
// Auth::routes();はLaravelが提供している便利な機能で
// 一般的な認証に関するルーティングを自動的に定義してくれます
// この一行を書くだけで、ログインやログアウト
// パスワードのリセット、新規ユーザー登録などのための
// ルートが作成されます。
// つまりログイン画面に用意されたビューのリンク先がこの1行で済みます
Route::group(['middleware' => 'auth'], function () {
Route::resource('products', ProductController::class);
});
一覧表示画面を作る(index.blade.php)
/ (ルート、プロジェクトディレクトリの最上位)
│
└── resources
│
└── views
│
└── products (もしくは別の任意のディレクトリ)
│
└── index.blade.php (サブディレクトリ内のビューファイル)
@extends('layouts.app')
@section('content')
<div class="container">
<h1 class="mb-4">商品情報一覧</h1>
<a href="{{ route('products.create') }}" class="btn btn-primary mb-3">商品新規登録</a>
<div class="products mt-5">
<h2>商品情報</h2>
<table class="table table-striped">
<thead>
<tr>
<th>商品名</th>
<th>メーカー</th>
<th>価格</th>
<th>在庫数</th>
<th>コメント</th>
<th>商品画像</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach ($products as $product)
<tr>
<td>{{ $product->product_name }}</td>
<td>{{ $product->company->company_name }}</td>
<td>{{ $product->price }}</td>
<td>{{ $product->stock }}</td>
<td>{{ $product->comment }}</td>
<td><img src="{{ asset($product->img_path) }}" alt="商品画像" width="100"></td>
</td>
<td>
<a href="{{ route('products.show', $product) }}" class="btn btn-info btn-sm mx-1">詳細表示</a>
<a href="{{ route('products.edit', $product) }}" class="btn btn-primary btn-sm mx-1">編集</a>
<form method="POST" action="{{ route('products.destroy', $product) }}" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm mx-1">削除</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endsection
コードの補足
@extends('layouts.app')
Laravelのビュー(画面)作成でよく使用されるBladeテンプレートエンジンの機能を使っています。他のビューの内容を「引き継ぐ」ための指示です。
具体的にはlayoutsディレクトリのapp.blase.phpというビューに書かれたHTMLやその他のコードを、このビューに引き継ぎ、その上に追加でコードを書いていきます。
ウェブサイト全体で共通の部分であるヘッダーやフッター、ナビゲーションメニューなどをlayouts.appなどのレイアウトビューに書いておき、それを各ページで引き継ぐことができます。その結果、同じコードを何度も書く手間を省くことができ、メンテナンスも容易になります。
layouts.appというビューは、Laravelの初期設定ではresources/views/layoutsディレクトリ
の下に配置されています。このビューはLaravelの認証機能を利用する際に自動的に生成されるもので、共通のヘッダーやフッター、CSSやJavaScriptのリンクなどが設定されています。
@section('content')
先ほど説明した@extendsと組み合わせて使うことが多いです。先程の例で言うと、@extends('layouts.app')でlayouts.appというビューの内容を引き継ぐわけですが、その上でどの部分にどの内容を追加するのかを指定するために@sectionが使われます。
@section('content')と書くと、「content」という名前のセクションを開始することを示します。そして、@section('content')から@endsectionまでの間に書かれたコードが、contentというセクションの内容となります。
このcontentという名前は、layouts.appなどのレイアウトビューの中で@yield('content')と書かれている部分に対応します。つまり、@section('content')から@endsectionまでの間に書かれたコードが、@yield('content')の部分に挿入されるわけです。
こうすることで、layouts.appなどのレイアウトビューの枠組みの中に、このビュー独自のコンテンツを挿入することができます。この機能を使うと、各ページの独自のコンテンツを書きながらも、全体のレイアウトは共通化できるので、コードの再利用性が高まり、メンテナンスも容易になります。
@foreach ($products as $product)
Bladeテンプレートエンジンで提供されている、繰り返し処理(ループ)を行うための記述方法です。具体的には、$productsという名前のリスト(配列)の中身を、1つずつ取り出して処理するための方法です。
コントローラーに記述していたindexメソッドにはビューを開く際に$products
という変数を使えるようにしていたため、そこから1レコードずつ情報を取り出していくために繰り返し処理を実行しています。$productsの中のアイテムの数だけレコードを表示するHTMLが繰り返されるわけです。
この機能を使うと、例えば商品の一覧を表示するために商品の数だけ同じようなHTMLを繰り返し書くという手間を省くことができます。$productsの中の各商品に対して、その商品の情報を表示するHTMLを生成するだけで、全ての商品の情報を表示することができます
href="{{ route('products.create') }}"
LaravelのBladeテンプレートエンジンでは、{{ }}という記述を使って、PHPのコードを書くことができます。この中に書かれたコードは、HTMLを生成するときに評価され、その結果がHTMLに埋め込まれます。
route関数を呼び出した場合、引数に指定されたルーティングの名前に対応するURLを生成します。つまりProductControllerのcreateメソッドを実行する
という意味になります
class="btn btn-primary mb-3"
BootstrapというCSSフレームワークを使って簡単にウェブページの見た目を整えるための機能を使用しています。
- btn: このクラスは要素をボタンのように見せる基本的なスタイルを適用します。
- btn-primary: このクラスはボタンに青色を適用しています
- mb-3: このクラスは要素の下側(margin-bottom)にスペースを追加します。
3はスペースの大きさを示しており、Bootstrapでは0から5までの値を用いて相対的なスペースの大きさを指定します。
Laravelでは、プロジェクトを作成した時点では直接Bootstrapは含まれていません。しかし、LaravelにはBootstrapを簡単にプロジェクトに追加する機能が提供されています。
Laravel UIというパッケージをインストールすることで、Bootstrapを含むフロントエンドのテンプレートをLaravelプロジェクトに追加できます。
composer require laravel/ui
そして、以下のコマンドを実行することで、BootstrapとVue.jsを含むフロントエンドのテンプレートを追加できます。
php artisan ui bootstrap --auth
これにより、BootstrapのCSSとJavaScript、そしてVue.jsがプロジェクトに追加され、さらに認証のためのビュー(ログイン画面や登録画面など)も自動的に生成されます。
{{ $product->product_name }}
phpを使ってproductsテーブルに存在するproduct_nameフィールドの情報を表示させています
{{ $product->company->name }}
productモデルとリレーション関係のcompanyモデルからnameフィールドの情報を取得しています
img src="{{ asset($product->img_path) }}
productsテーブルに保存されているimg_pathフィールドのURLを取得します
asset関数の引数にしない場合は$product->img_path
がそのまま文字列として扱われます
route('products.show', $product)
ProductControllerのshowメソッドが呼び出しproduct(インスタンス)を渡しています。
$productにはID情報が含まれているためコントローラーのshowメソッドによりIDが自動取得され、商品IDに関する詳細ページを開くことができます
コントローラー側の話になりますが、public function myMethod(Product $product) {
のように書くことで自動的にインスタンスが取得される仕組み(Route Model Binding)が存在します。
(モデル名 $変数名)
と書くことでメソッドの名前がshowであろうと何であろうとLaravelはその引数に対応をするインスタンスを自動的に取得してくれます
destroyメソッドについて
次のようにデータを削除するボタンはフォームで作成されています
<form method="POST" action="{{ route('products.destroy', $product) }}" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm mx-1">削除</button>
</form>
次のコマンドはLaravelのルーティングを確認するコマンドです
php artisan route:list
レコードを削除するルーティングは以下でしたので、$product(インスタンス)を送るためPOST通信でデータを送り、さらにリクエストをDELETE
に切り替える必要があります
DELETE products/{product} ......... products.destroy › ProductController@destroy
@csrfについて
CSRF(クロスサイトリクエストフォージェリ)保護トークンを生成します。つまり違法なサイトからの通信ではないことを証明してくれるトークンであり、これはセキュリティ上重要な機能で、フォームからのリクエストが本物であることを確認します。このトークンがないと、LaravelはフォームからのPOSTリクエストを拒否しまてしまうため、例えばブラウザ画面から"削除"ボタンを押しても途中で通信が止まってしまい、データベースが変更されません。formタグを使った通信にはもれなく@csrfを使う必要があります
新規作成画面を作る(create.blade.php)
@extends('layouts.app')
@section('content')
<div class="container">
<h1 class="mb-4">商品新規登録</h1>
<a href="{{ route('products.index') }}" class="btn btn-primary mb-3">商品一覧に戻る</a>
<form method="POST" action="{{ route('products.store') }}" enctype="multipart/form-data">
@csrf
<div class="mb-3">
<label for="product_name" class="form-label">商品名:</label>
<input id="product_name" type="text" name="product_name" class="form-control" required>
</div>
<div class="mb-3">
<label for="company_id" class="form-label">メーカー</label>
<select class="form-select" id="company_id" name="company_id">
@foreach($companies as $company)
<option value="{{ $company->id }}">{{ $company->company_name }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="price" class="form-label">価格:</label>
<input id="price" type="text" name="price" class="form-control" required>
</div>
<div class="mb-3">
<label for="stock" class="form-label">在庫数:</label>
<input id="stock" type="text" name="stock" class="form-control" required>
</div>
<div class="mb-3">
<label for="comment" class="form-label">コメント:</label>
<textarea id="comment" name="comment" class="form-control" rows="3" required></textarea>
</div>
<div class="mb-3">
<label for="img_path" class="form-label">商品画像:</label>
<input id="img_path" type="file" name="img_path" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">登録</button>
</form>
</div>
@endsection
新規作成画面(create.blade.php)の補足
@csrfについては前述のとおりです
action属性によってformがProductControllerのstoreメソッドにデータを送信していることがわかります。
<form method="POST" action="{{ route('products.store') }}" enctype="multipart/form-data">
@csrf
<input id="product_name" type="text" name="product_name" class="form-control" required>
// 残りのコード
enctype="multipart/form-data"
画像ファイルをアップロードする場合に必ず必要な属性です。もしenctype="multipart/form-data"がなければ、フォームはただのテキストしか送れません。非テキストデータを含む可能性がある場合に使用されます。
inputについて補足
<input id="product_name" type="text" name="product_name" class="form-control" required>
- id="product_name"
この要素はHTML内でこの入力フィールドを特定するためのユニークな識別子を提供します。JavaScriptやCSSでこの入力フィールドを操作する際に使用します。 - type="text"
この属性はテキスト入力フィールドを指定します。ユーザーはここにテキストを入力できます。 - name="product_name"
この属性はサーバーに送信されるデータの名前を指定します。フォームが送信されると、この名前のデータがサーバーに送られます。つまりコントローラー(storeメソッド)で使われているフィールド名はこのnameを参照しています - class="form-control"
これはBootstrapのクラスで、スタイリングとレイアウトのために使用されます。BootstrapはCSSフレームワークで、フォームの見た目を整えるための多くの便利なスタイルを提供しています。 - required
この属性はこの入力フィールドが必須であることを指定します。つまり、ユーザーがこのフィールドに何かを入力しなければならないということです。この属性が指定されていると、ユーザーが何も入力せずにフォームを送信しようとすると、ブラウザは警告メッセージを表示します。
編集画面を作る(edit.blade.php)
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<a href="{{ route('products.index') }}" class="btn btn-primary mt-1 mb-3">商品一覧画面に戻る</a>
<div class="card">
<div class="card-header"><h2>商品情報を変更する</h2></div>
<div class="card-body">
<form method="POST" action="{{ route('products.update', $product) }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="mb-3">
<label for="product_name" class="form-label">商品名</label>
<input type="text" class="form-control" id="product_name" name="product_name" value="{{ $product->product_name }}" required>
</div>
<div class="mb-3">
<label for="company_id" class="form-label">会社</label>
<select class="form-select" id="company_id" name="company_id">
@foreach($companies as $company)
<option value="{{ $company->id }}" {{ $product->company_id == $company->id ? 'selected' : '' }}>{{ $company->company_name }}</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label for="price" class="form-label">金額</label>
<input type="number" class="form-control" id="price" name="price" value="{{ $product->price }}" required>
</div>
<div class="mb-3">
<label for="stock" class="form-label">在庫数</label>
<input type="number" class="form-control" id="stock" name="stock" value="{{ $product->stock }}" required>
</div>
<div class="mb-3">
<label for="comment" class="form-label">コメント</label>
<textarea id="comment" name="comment" class="form-control" rows="3">{{ $product->comment }}</textarea>
</div>
<div class="mb-3">
<label for="img_path" class="form-label">商品画像:</label>
<input id="img_path" type="file" name="img_path" class="form-control">
<img src="{{ asset($product->img_path) }}" alt="商品画像" class="product-image">
</div>
<button type="submit" class="btn btn-primary">変更内容で更新する</button>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
詳細表示画面を作る(show.blade.php)
@extends('layouts.app')
@section('content')
<div class="container">
<h1 class="mb-4">商品情報詳細</h1>
<a href="{{ route('products.index') }}" class="btn btn-primary mt-3">商品一覧画面に戻る</a>
<dl class="row mt-3" >
<dt class="col-sm-3">商品情報ID</dt>
<dd class="col-sm-9">{{ $product->id }}</dd>
<dt class="col-sm-3">商品画像</dt>
<dd class="col-sm-9">{{ $product->product_name }}</dd>
<dt class="col-sm-3">メーカー</dt>
<dd class="col-sm-9">{{ $product->company->name }}</dd>
<dt class="col-sm-3">価格</dt>
<dd class="col-sm-9">{{ $product->price }}</dd>
<dt class="col-sm-3">在庫数</dt>
<dd class="col-sm-9">{{ $product->stock }}</dd>
<dt class="col-sm-3">コメント</dt>
<dd class="col-sm-9">{{ $product->comment }}</dd>
<dt class="col-sm-3">商品画像</dt>
<dd class="col-sm-9"><img src="{{ asset($product->img_path) }}" width="300"></dd>
</dl>
<a href="{{ route('products.edit', $product) }}" class="btn btn-primary btn-sm mx-1">商品情報を編集する</a>
</div>
@endsection
view画面の確認(途中経過)
マイグレーションでテーブルを作成し、MVCそれぞれのファイルにこれまで紹介したコードを記述すると次のような画面になるはずです。
ダミーデータの作成
動作確認をするためにはテーブルにダミーとなるデータが必要です。ただし1〜2件ならSQLで追加することも容易ですが100件〜200件単位のダミーデータが必要な場合は手間がかかるので、Laravelではダミーデータを自動挿入することができます
【補足】トレイトを使ってダミーデータを挿入する
以下の手順はLaravel 8から実行できる手順になります。まずトレイトとはLaravelに標準搭載されている機能の一つで色々な機能や方法をまとめて保管するためのツールです。その中のHasFactoryトレイト
を使用してダミーデータを挿入することができます
① モデルの準備
まず、モデルにHasFactoryトレイトを使うための準備をします。
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
}
上記のように、モデル内でHasFactoryトレイトをuseキーワードで取り込みます。
② ファクトリの生成
次に、モデルのファクトリを生成します。ファクトリとは、ダミーデータを生成するための「道具」のようなもので具体的なイメージとしてはデータの「型」を設定しておき、その「型」に従って大量のデータを生成するイメージになります
php artisan make:factory ProductFactory --model=Product
php artisan make:factory CompanyFactory --model=Company
php artisan make:factory SaleFactory --model=Sale
このコマンドを実行するとdatabase/factories
にProductFactory.php
が作成されます。
③ ファクトリの定義
ProductFactory.phpを開き、ダミーデータの定義をします。
リレーション関係(外部キー)を設定しているため3つセットでダミーファイルを作成する必要があります
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\Product; // この行を追加
use App\Models\Company; // この行を追加
class ProductFactory extends Factory
{
protected $model = Product::class; // この行を追加
public function definition(): array
{
return [
'company_id' => Company::factory(),
'product_name' => $this->faker->word, // ダミーの商品名
'price' => $this->faker->numberBetween(100, 10000), // 100から10,000の範囲のダミー価格
'stock' => $this->faker->randomDigit, // 0から9のランダムな数字でダミーの在庫数
'comment' => $this->faker->sentence, // ダミーの説明文
'img_path' => 'https://picsum.photos/200/300', // 200x300のランダムな画像
];
}
}
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class CompanyFactory extends Factory
{
public function definition(): array
{
return [
'company_name' => $this->faker->company,
'street_address' => $this->faker->streetAddress,
'representative_name' => $this->faker->name,
// 'created_at' と 'updated_at' はEloquentが自動的に処理するので、ここに追加する必要はありません。
];
}
}
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class SaleFactory extends Factory
{
public function definition(): array
{
return [
'product_id' => \App\Models\Product::factory(), // 仮定しているProductモデルのファクトリーを利用
// 'created_at' と 'updated_at' はEloquentが自動的に処理するので、ここに追加する必要はありません。
];
}
}
③ Seederの作成
シーダーは、Laravelのデータベースに初期データやテストデータを挿入するためのスクリプト(簡易的なプログラム)です
php artisan make:seeder ProductsTableSeeder
php artisan make:seeder CompaniesTableSeeder
php artisan make:seeder SalesTableSeeder
このコマンドを実行すると database/seeders/ProductsTableSeeder.php が生成されます
④ Seederの修正
ProductsTableSeederファイルを開き、ダミーデータの挿入方法を定義します。他2つのファイルも同様のコードを入力します
<?php
namespace Database\Seeders;
use App\Models\Product; //追加
use Illuminate\Database\Seeder;
class ProductsTableSeeder extends Seeder
{
public function run()
{
//Productモデルのファクトリーを使ってダミーレコードを10件作成
Product::factory()->count(10)->create();
}
}
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Company; //追加
class CompaniesTableSeeder extends Seeder
{
public function run(): void
{
Company::factory()->count(10)->create();
}
}
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Sale; //追加
class SalesTableSeeder extends Seeder
{
public function run(): void
{
// ダミーレコードを10件作成
Sale::factory()->count(10)->create();
}
}
⑤ DatabaseSeederの修正
次のコマンドでダミーデータを作成しますが、コマンドで呼び出すSeederをここで指定します
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run()
{
$this->call(ProductsTableSeeder::class);
$this->call(CompaniesTableSeeder::class);
$this->call(SalesTableSeeder::class);
// 他のシーダーも呼び出す場合は、ここに追加します。
}
}
⑥シーダーを実行する
Laravelフレームワークにおいて、データベースにダミーデータを追加するコマンドです。具体的にはdatabase/seeders
にあるDatabaseSeeder
というメインのシーダーを実行し、このシーダーから他のシーダーを呼び出すことでダミーを追加するプログラムを動かします
php artisan db:seed
次の画像のようにコマンドが正常に稼働すればダミーデータがデータベースに保存されます
シンボリックリンク(保存した画像を表示する)
create.blade.php
で新しく商品登録をした場合、選択した画像はLaravelプロジェクトのstorage
に保存されます。同時に保存先のパスはproductsテーブルのimg_pathフィールドに保存されますが次のようなパスなので、このままでは画像をブラウザに表示させることができません
/storage/products/ファイル名.png
以下のダミーで用意した画像のURLはネット経由で画像を取得しているため、表示することができています
シンボリックリンクを貼る
シンボリックリンクとはLaravelのコマンドであり、シンボリックリンク(ショートカットのようなもの)を作成することでstorage/app/public
に保存されたファイルに対して公開アクセスが可能になります。
php artisan storage:link
これでindex.blade.php
およびshow.blade.php
のimgタグから画像のURLを検索しブラウザに画像を表示させることができるようになります
ページネーションを実装する
ページネーション(Pagination)とは、長いリストやコンテンツ群を複数のページに分割して表示する技法のことを指します。
実装する方法はコントローラーの対象となるデータを取得する際に、paginateメソッドを使用し、ビューでページネーションリンクを表示するには、ページネーションオブジェクトの links メソッドを使用します。
例えばProductモデルから10件のデータをページごとに取得する場合は以下のようになります。
// 10件ずつの商品情報を取得します。
$products = Product::paginate(10);
{{ $products->links() }}
コードを書き換える(コントローラーとビュー)
public function index()
{
$products = Product::paginate(10);
return view('products.index', compact('products'));
}
@extends('layouts.app')
@section('content')
<div class="container">
<h1 class="mb-4">商品情報一覧</h1>
<a href="{{ route('products.create') }}" class="btn btn-primary mb-3">商品新規登録</a>
<div class="products mt-5">
<h2>商品情報</h2>
<table class="table table-striped">
<thead>
<tr>
<th>商品名</th>
<th>メーカー</th>
<th>価格</th>
<th>在庫数</th>
<th>コメント</th>
<th>商品画像</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach ($products as $product)
<tr>
<td>{{ $product->product_name }}</td>
<td>{{ $product->company->name }}</td>
<td>{{ $product->price }}</td>
<td>{{ $product->stock }}</td>
<td>{{ $product->comment }}</td>
<td><img src="{{ asset($product->img_path) }}" alt="商品画像" width="100"></td>
</td>
<td>
<a href="{{ route('products.show', $product) }}" class="btn btn-info btn-sm mx-1">詳細表示</a>
<a href="{{ route('products.edit', $product) }}" class="btn btn-primary btn-sm mx-1">編集</a>
<form method="POST" action="{{ route('products.destroy', $product) }}" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm mx-1">削除</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{ $products->links() }}
</div>
@endsection
ページネーションの大きさがおかしい場合の対処
ページネーションを実装してもアイコンが大きすぎる場合があります。
次のリンク先に解決策を紹介しているので発生した場合は参照してください
一覧画面から絞り込み検索する方法について
indexの画面に検索フォームを追加することでレコードを絞り込むことができ、管理画面がより扱いやすくなります
【補足】inputに関する属性の説明
<form action="{{ route('products.index') }}" method="GET">
<label>商品名:</label>
<input type="text" name="search" value="{{ request('search') }}">
<label>最小価格:</label>
<input type="number" name="min_price" value="{{ request('min_price') }}">
<label>最大価格:</label>
<input type="number" name="max_price" value="{{ request('max_price') }}">
<label>最小在庫:</label>
<input type="number" name="min_stock" value="{{ request('min_stock') }}">
<label>最大在庫:</label>
<input type="number" name="max_stock" value="{{ request('max_stock') }}">
<button type="submit">検索</button>
</form>
-
action="{{ route('products.index') }}"
ProductControllerのindexメソッドにデータを送信しています。後述のコントローラーの処理で検索キーワードが存在する場合はレコードの絞り込みをしてindexを表示させます(検索キーワードがなければ全てのレコードを表示します) -
method="GET"
ページを更新した場合、URLに検索キーワードが含まれているため、後述の仕組みでフォームに検索キーワードを残すことができます -
value="{{ request('search') }}"
URLが http://example.com/products?search=shirt だった場合、request('search') はshirt
を返します。したがってvalue="{{ request('search') }}"
は、ユーザーが以前に入力した検索キーワードをフォームに事前に表示するためのものです。ユーザーが検索条件を変更して再検索したい場合、前回の検索キーワードが既に入力されているので便利です。 -
name="search"
テキストボックスの入力内容をコントローラーへsearch
という名前で送信するための指示となるものです。コントローラー側では
$request->search
という形で中身を参照できます
検索フォームを追加する(ビュー)
<!-- 検索フォームのセクション -->
<div class="search mt-5">
<!-- 検索のタイトル -->
<h2>検索条件で絞り込み</h2>
<!-- 検索フォーム。GETメソッドで、商品一覧のルートにデータを送信 -->
<form action="{{ route('products.index') }}" method="GET" class="row g-3">
<!-- 商品名検索用の入力欄 -->
<div class="col-sm-12 col-md-3">
<input type="text" name="search" class="form-control" placeholder="商品名" value="{{ request('search') }}">
</div>
<!-- 最小価格の入力欄 -->
<div class="col-sm-12 col-md-2">
<input type="number" name="min_price" class="form-control" placeholder="最小価格" value="{{ request('min_price') }}">
</div>
<!-- 最大価格の入力欄 -->
<div class="col-sm-12 col-md-2">
<input type="number" name="max_price" class="form-control" placeholder="最大価格" value="{{ request('max_price') }}">
</div>
<!-- 最小在庫数の入力欄 -->
<div class="col-sm-12 col-md-2">
<input type="number" name="min_stock" class="form-control" placeholder="最小在庫" value="{{ request('min_stock') }}">
</div>
<!-- 最大在庫数の入力欄 -->
<div class="col-sm-12 col-md-2">
<input type="number" name="max_stock" class="form-control" placeholder="最大在庫" value="{{ request('max_stock') }}">
</div>
<!-- 絞り込みボタン -->
<div class="col-sm-12 col-md-1">
<button class="btn btn-outline-secondary" type="submit">絞り込み</button>
</div>
</form>
</div>
<!-- 検索条件をリセットするためのリンクボタン -->
<a href="{{ route('products.index') }}" class="btn btn-success mt-3">検索条件を元に戻す</a>
検索フォームを追加する(コントローラ)
ビューから送られた情報はコントローラーのindexメソッドに送信されています。本来indexメソッドは全てのレコードを取得するメソッドとして利用していましたが、ifを使って検索キーワードが存在する場合はその分、段々と絞り込みをかける要求を作ります
データベースに対して検索、追加、更新、削除などの操作要求をクエリと呼びます
public function index(Request $request)
{
// Productモデルに基づいてクエリビルダを初期化
$query = Product::query();
// この行の後にクエリを逐次構築していきます。
// そして、最終的にそのクエリを実行するためのメソッド(例:get(), first(), paginate() など)を呼び出すことで、データベースに対してクエリを実行します。
// 商品名の検索キーワードがある場合、そのキーワードを含む商品をクエリに追加
if($search = $request->search){
$query->where('product_name', 'LIKE', "%{$search}%");
}
// 最小価格が指定されている場合、その価格以上の商品をクエリに追加
if($min_price = $request->min_price){
$query->where('price', '>=', $min_price);
}
// 最大価格が指定されている場合、その価格以下の商品をクエリに追加
if($max_price = $request->max_price){
$query->where('price', '<=', $max_price);
}
// 最小在庫数が指定されている場合、その在庫数以上の商品をクエリに追加
if($min_stock = $request->min_stock){
$query->where('stock', '>=', $min_stock);
}
// 最大在庫数が指定されている場合、その在庫数以下の商品をクエリに追加
if($max_stock = $request->max_stock){
$query->where('stock', '<=', $max_stock);
}
// 上記の条件(クエリ)に基づいて商品を取得し、10件ごとのページネーションを適用
$products = $query->paginate(10);
// 商品一覧ビューを表示し、取得した商品情報をビューに渡す
return view('products.index', ['products' => $products]);
}
ページネーションをクリックした際に検索条件を維持する
これまでに実装していたページネーションのタグでは、検索条件で絞り込んだ後にページネーションのリンク先にぺージを移動させると検索条件が保持されず、全てのレコードが再度表示されてしまいます。
appends()メソッドはappendsは「ページを移動しても条件を忘れないようにする」メソッドになります。今回は絞り込み条件を付与しているので、appendsメソッドを使うことで次のリンクをクリックしても現在の条件を忘れることはありません
以下のappends(request()->query())部分
は、現在のリクエストのすべてのクエリストリングをページネーションリンクに追加します。したがって、下限や上限などのクエリで絞り込んだ状態を維持しつつページネーションを利用できます。
{{ $products->links() }} // 変更前
{{ $products->appends(request()->query())->links() }} //変更後
実装を確認する
各々のカラムにソート機能を追加する
テーブルのそれぞれの列を昇順や降順で並び替える機能を追加するには、まずコントローラーに昇順・降順を行うクエリを追加することが必要です。クエリとは、データベースから特定の情報を絞り込みしたり、特定の条件で並び替えしたりするための手順や命令のことを指しますので、具体的にはProductController
のindexメソッド
内に、データを昇順や降順に並び替えるためのクエリを追加します。そして、フロントエンド(ビュー)で提供されるボタンやリンクを使ってこのクエリを呼び出すことで、ユーザーが並び順を変更できるようになります。
public function index(Request $request)
{
// Productモデルに基づいてクエリビルダを初期化
$query = Product::query();
// この行の後にクエリを逐次構築していきます。
// そして、最終的にそのクエリを実行するためのメソッド(例:get(), first(), paginate() など)を呼び出すことで、データベースに対してクエリを実行します。
// 商品名の検索キーワードがある場合、そのキーワードを含む商品をクエリに追加
if($search = $request->search){
$query->where('product_name', 'LIKE', "%{$search}%");
}
// 最小価格が指定されている場合、その価格以上の商品をクエリに追加
if($min_price = $request->min_price){
$query->where('price', '>=', $min_price);
}
// 最大価格が指定されている場合、その価格以下の商品をクエリに追加
if($max_price = $request->max_price){
$query->where('price', '<=', $max_price);
}
// 最小在庫数が指定されている場合、その在庫数以上の商品をクエリに追加
if($min_stock = $request->min_stock){
$query->where('stock', '>=', $min_stock);
}
// 最大在庫数が指定されている場合、その在庫数以下の商品をクエリに追加
if($max_stock = $request->max_stock){
$query->where('stock', '<=', $max_stock);
}
// ソートのパラメータが指定されている場合、そのカラムでソートを行う
if($sort = $request->sort){
$direction = $request->direction == 'desc' ? 'desc' : 'asc'; // directionがdescでない場合は、デフォルトでascとする
$query->orderBy($sort, $direction);
}
// 上記の条件(クエリ)に基づいて商品を取得し、10件ごとのページネーションを適用
$products = $query->paginate(10);
// 商品一覧ビューを表示し、取得した商品情報をビューに渡す
return view('products.index', ['products' => $products]);
}
appendsメソッドについて
appendsはLaravelのページネーションインスタンスに属するメソッドです。
paginateを使うことで例えば、1000件の商品データがあるとして、1ページに10件ずつ表示したい場合、paginate(10)というコードを使います。
このとき、ページ下部に「1, 2, 3, ...」というページの番号が表示されるリンクが作られます。これをクリックすることで次のページや前のページに移動できます。
しかし、何らかの条件でデータを絞り込んで表示している場合(例:「青い商品だけを表示」)、その条件を保持しつつ次のページや前のページに移動したいですよね。
appendsメソッドは、そういったときに役立ちます。このメソッドを使うことで、ページのリンクに「現在の条件」をくっつけることができます。これにより、次のページや前のページに移動しても、その条件が維持されたままとなります。
簡単に言うと、appendsは「ページを移動しても条件を忘れないようにする」ための方法ですが
このメソッドはフロントエンド(ビュー)かコントローラーどちらかに記述しておけば機能します。
フロントエンド(ビュー)の場合
{{ $products->links() }} // appendsを使わない場合
{{ $products->appends(request()->query())->links() }} //appendsを使う場合
appends(request()->query())
は現在のクエリパラメータ(例: ソート条件やフィルタリング条件など)を保持した状態でのページネーションリンクを生成することができます。これにより、ユーザがページネーションのリンクをクリックしても、適用されているソートやフィルタリングの条件が維持されたままページ移動が行われます。
$products->links()の間に上記のメソッドを置くことでappendsメソッドが適用されます
バックエンド(コントローラー)の場合
$products = $query->paginate(10); // appendsを使わない場合
// appendsを使う場合
$products = $query->paginate(10)->appends($request->all());
ビューかコントローラー側どちらか
にappendsを記述しておくことで、ページネーションのリンクをクリックしても検索条件やソートが保持されるようになります
$request-allについて
$request->all() は、リクエストに含まれているすべてのデータを取得するLaravelのメソッドです。このコードの中では、現在のリクエストに含まれるすべてのクエリパラメータをページネーションのリンクに追加しています。
要するに、この行のコードは「データベースから商品の情報を10件ずつ取得し、それをページに分けて、さらに現在の絞り込み条件や並び替えの条件をページネーションのリンクにも適用する」という操作を行っています。
ソート機能動作確認(1)
上記まででコントローラーのソートに関するコードは書き終わりました。ここで試しに以下のURLを調べた場合に昇順・降順のメソッドが実行されるか確認します
昇順の場合
降順の場合
クエリパラメータについて
URLに付与された以下のクエリパラメーターがprice
を昇順・降順に並べ替えます
URL ?sort=price&direction=desc
URLの「?」以降に続く部分はクエリ文字列として渡されるため、その中に含まれる一連の「キー = 値」のペアがクエリパラメータです。
この例ではsort=price
と direction=asc
の2つのクエリパラメータがあります。sort
と direction
はクエリパラメータのキーで、それぞれのキーには price
と asc
という値が割り当てられています。
コントローラーでの動きについて
Get通信のクエリパラメータはindexメソッドの引数に代入され$requestで使用できるようにされています。
public function index(Request $request)
{
// Productモデルに基づいてクエリビルダを初期化
$query = Product::query();
// 省略
if($sort = $request->sort){
$direction = $request->direction == 'desc' ? 'desc' : 'asc';
// もし $request->direction の値が 'desc' であれば、'desc' を返す。
// そうでなければ'asc' を返す
$query->orderBy($sort, $direction);
// orderBy('カラム名', '並び順')
}
// 上記の条件(クエリ)に基づいて商品を取得し、10件ごとのページネーションを適用
$products = $query->paginate(10);
// 商品一覧ビューを表示し、取得した商品情報をビューに渡す
return view('products.index', ['products' => $products]);
}
プログラムは次のような仕組みになっています
- もしも$request->sortが空でなければソート処理をスタートする
- 昇順(asc)と降順(desc)を判断する
- クエリに昇順・降順を追加する
- ページネーションの部分でクエリに沿ってproductsテーブルを取得する
- ビューに取得した情報を渡す
ビュー側の変更コード
以下の方法は非常に単純な仕組みとなっております。JavaScrptやformタグなどを使えばaタグを使わずボタンを用意する他、動的な処理も可能です。
<tr>
<th>商品名</th>
<th>メーカー</th>
<th>価格
<a href="{{ request()->fullUrlWithQuery(['sort' => 'price', 'direction' => 'asc']) }}">↑</a>
<a href="{{ request()->fullUrlWithQuery(['sort' => 'price', 'direction' => 'desc']) }}">↓</a>
</th>
<th>
在庫数
<a href="{{ request()->fullUrlWithQuery(['sort' => 'stock', 'direction' => 'asc']) }}">↑</a>
<a href="{{ request()->fullUrlWithQuery(['sort' => 'stock', 'direction' => 'desc']) }}">↓</a>
</th>
<th>コメント</th>
<th>商品画像</th>
<th>操作</th>
</tr>
fullUrlWithQuery メソッドについて
fullUrlWithQueryは、現在のURLに新しいクエリパラメータを追加または上書きして返します。この方法であれば、他のクエリパラメータ(最大値、最小値、キーワード検索など)も維持しつつ、ソート条件だけを変更できます。
つまり、リクエストオブジェクトにアクセスし、リンク先のindexメソッドにクエリパラメータを渡している状況になります
- "{{ }}" の中にはPHPの式を書くことができます。そのため、変数だけでなく関数やメソッドの呼び出しも可能です。
ソート機能動作確認(2)
価格と在庫数の列に用意した矢印をクリックすることで、昇順と降順を切り替えられるようになっています。
Githubのリポジトリ
追加機能について
APIの実装とPostmanでの確認
まとめ
以上、詳細に実装手順やコードの詳細をまとめさせて頂きました。至らない所が多々あるかと存じますが表現方法や構成についても学んでいきますので、別の記事につきましても参考になれば幸いです。