トップページ
Laravel学習サイトLaravelやるばい

目次

商品の登録・編集・削除・表示

今回からECサイトの作成になります。

数回に渡って解説を行います。

解説は下記の内容に分けます。

それでは説明を始めます。


最初にすること

Breezeでログインができるようにしてユーザーの登録をして下さい。


商品の登録

今までの復習ばかりです。

まずはコントローラーを作成します。

コントローラー名を「Shop」とします。

下記のコマンドを叩きます。

php artisan make:controller Shop

また商品情報を登録するテーブルを作成します。

テーブル名を「stocks」とします。

下記のコマンドを叩きます。

php artisan make:model Stock -m

商品の情報を登録するのに必要なのは下記になります。

カラム名とデータ型は下記とします。

マイグレーションファイルのupメソッドに下記の記述をします。

public function up(): void
{
    Schema::create('stocks', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('image');
        $table->string('detail');
        $table->integer('price');
        $table->timestamps();
    });
}

記述が終わったら下記のコマンドを叩きます。

php artisan migrate

そしてStockモデルにカラムの登録ができるようにする為の記述をします。

class Stock extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'image',
        'detail',
        'price'
    ];
}

それでは商品登録ページにアクセスして商品の登録をします。

まずは商品の登録ページにアクセスできるようにします。

web.phpに下記の記述をします。

Route::middleware('auth')->group(function () {
    Route::controller(ShopController::class)->group(function () {
        Route::get('/stock/create', 'create')->name('shop.create');
    });
});

Shopコントローラーに下記の記述をします。

public function create() {
    return view('shop.create');
}

そしてshopフォルダを作成してその下にcreate.blade.phpを作成して下記の記述をします。

<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>商品登録ページ</title>
    </head>
    <body>
        <div class="product-register-container">
            @if(session('message'))
                <div class="flash">
                    {{ session('message') }}
                </div>
            @endif
            @if($errors->any())
            <ul class="error">
                @foreach($errors->all() as $error)
                    <li>{{$error}}aa</li>
                @endforeach
            </ul>
            @endif
            <h1 class="item-name">商品登録</h1>
            <form action="" method="post" enctype="multipart/form-data">
                @csrf
                <div class="title-wrap">商品タイトル : <input type="text" name="name" placeholder="商品タイトル" value="{{ old('name') }}"></div>
                <div class="image-wrap">商品画像 : <input type="file" name="image" accept="image/jpg,image/png"></div>
                <div class="detail-wrap">商品説明 : <textarea type="text" name="detail" cols="40" rows="10" placeholder="商品説明">{{ old('detail') }}</textarea></div>
                <div class="price-wrap">値段 : <input type="text" name="price" placeholder="10000" value="{{ old('price') }}"></div>
                <div class="submit-wrap"><input type="submit" value="登録する"></div>
            </form>
        </div>
    </body>
</html>

<style>
    .product-register-container {
        width: 80%;
        margin-left: auto;
        margin-right: auto;
    }

    .item-name {
        font-size: 18px;
        text-align: center;
    }
    @media screen and (min-width: 1023px) {
        .item-name {
            font-size: 25px;
        }
    }

    .title-wrap, .image-wrap, .detail-wrap, .price-wrap {
        font-size: 16px;
        margin-bottom: 20px;
    }
    @media screen and (min-width: 1023px) {
        .title-wrap, .image-wrap, .detail-wrap, .price-wrap {
            font-size: 16px;
        }
    }
    .flash {
        font-size: 16px;
        border: 1px solid red;
        padding: 10px;
        text-align: center;
    }
    @media screen and (min-width: 1023px) {
        .flash {
            font-size: 16px;
        }
    }
</style>

表示に関してスマホとパソコンの両方の画面に対応させています。

下記の表示になります。

20250303_162820_ec1.jpg

このサイトに載っているアプリ解説と記述がほぼ同じですが下記の説明をまだしてないので説明します。

<form action="" method="post" enctype="multipart/form-data">

情報を登録する時に登録するだけではなく「このファイルはこんな種類ですよ」という情報も送信しています。

ファイルの種類のことをMIMEタイプと呼びます。

例えば下記があります。

そして複数のファイルを一度に送信できるMIME TYPEが「multipart/form-data」です。

テキストと画像を同時に送る時に必要になります。

今までのアプリ作成では画像の登録がなかったから「multipart/form-data」の記述がありませんでした。

それではテーブルに値を保存する為の処理をモデルに記述します。

Stockモデルに下記の記述をします。

class Stock extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'image',
        'detail',
        'price'
    ];

    public function itemStore($request) {
        $inputs = $request->validate([
            'name' => 'required',
            'detail' => 'required',
            'price' => 'required',
            'image' => 'required',
        ]);

        $this->name = $inputs['name'];
        $this->detail = $inputs['detail'];
        $this->price = $inputs['price'];
        $original_name = $request->file('image')->getClientOriginalName();
        $name = date('Ymd_His').'_'.$original_name;
        $request->file('image')->move('storage/images',$name);
        $this->image = $name;
        $this->save();
    }
}

下記のコードが初めて登場しました。

$original_name = $request->file('image')->getClientOriginalName();
$name = date('Ymd_His').'_'.$original_name;
$request->file('image')->move('storage/images',$name);

1行目の記述ですが画像のファイル名を登録すると別の名前で登録されますが元々の名前で登録したいですよね。

元々の名前で登録する為に「getClientOriginalName」メソッドの記述があります。

2行目はファイル名の前に日付を付けています。

日付でなくてもランダムな文字列を付ければいいです。

ファイル名の前に文字列を付ける理由はファイル名だけを登録すると万が一同じファイル名を登録した時に上書きされるのですがそれを防ぐ為です。

3行目を説明する前に説明しないといけないことがあります。

Laravelは仕様上publicフォルダ以外はアクセスできなくなっています。

セキュリティ上画像はアプリ直下のstorageフォルダに保存されますがpublicフォルダの外にあるのでアクセスできません。

20250303_173442_ec2.jpg

publicフォルダの中からstorageフォルダにアクセスする為にシンボリックリンクを作成しないといけません。

下記のコマンドを叩きます。

php artisan storage:link

これでpublicフォルダの中にstorageのアイコンが作成されるのを確認できます。

20250303_173810_ec3.jpg

モデルのコードの話に戻します。

$request->file('image')->move('storage/images',$name);」で「ec > storage > app > public > images」の下に画像を保存するという意味です。

下記の記述は画像をLaravelのプロジェクトに保存する為の記述でした。

$original_name = $request->file('image')->getClientOriginalName();
$name = date('Ymd_His').'_'.$original_name;
$request->file('image')->move('storage/images',$name);

モデルのstoreメソッドをShopコントローラーで使います。

Shopコントローラーの記述を下記にします。

use App\Models\Stock;
         ・
         ・


public function store(Stock $stock, Request $request) {
    $stock->itemStore($request);
    return back()->with('message','商品の投稿が完了しました');
}

storeメソッドを動作させる為にweb.phpの記述をします。

下記の追記をして下さい。

Route::middleware('auth')->group(function () {
    Route::controller(ShopController::class)->group(function () {
        Route::get('/stock/create', 'create')->name('shop.create');

        Route::post('/stock/store', 'store')->name('shop.store');
    });
});

「shop.store」をcreate.blade.phpに追記します。

下記の追記をして下さい。

<form action="{{ route('shop.store') }}" method="post" enctype="multipart/form-data">       //この行を修正
    @csrf
    <div class="title-wrap">商品タイトル : <input type="text" name="name" placeholder="商品タイトル" value="{{ old('name') }}"></div>
    <div class="image-wrap">商品画像 : <input type="file" name="image" accept="image/jpg,image/png"></div>
    <div class="detail-wrap">商品説明 : <textarea type="text" name="detail" cols="40" rows="10" placeholder="商品説明">{{ old('detail') }}</textarea></div>
    <div class="price-wrap">値段 : <input type="text" name="price" placeholder="10000" value="{{ old('price') }}"></div>
    <div class="submit-wrap"><input type="submit" value="登録する"></div>
</form>

これで商品の登録ができます。

試しに登録してみて下さい。

成功すると下記の表示になります。

20250303_182303_ec5.jpg

実装としては問題ないですが商品情報を登録したらその商品の編集ページにリダイレクトして欲しいと思いませんか?

この記事の最後におまけをつけますのでそこで装方法の説明をします。

次は商品情報を表示します。


商品情報の表示

web.phpに下記の記述をします。

Route::middleware('auth')->group(function () {
    Route::controller(ShopController::class)->group(function () {
        Route::get('/stock/create', 'create')->name('shop.create');

        Route::post('/stock/store', 'store')->name('shop.store');

        Route::get('/index', 'index')->name('shop.index');
    });
});

Stockモデルに下記の記述をします。

public function itemIndex() {
    return Stock::all();
}

Shopコントローラーに下記の記述をします。

public function index(Stock $stock) {
    $items = $stock->itemIndex();

    return view('shop.index', compact('items'));
}

shopフォルダの下にindex.blade.phpを作成して下記の記述をします。

<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>商品一覧</title>
    </head>
    <body>
        <div class="item-container">
            <h1>商品一覧</h1>
            <div class="item-wrap">
                @forelse($items as $item)
                    <div class="item">
                        <h2>{{ $item->name }}</h2>
                        <img src="{{ asset('storage/images/' . $item->image) }}" alt="">
                        <p>{{ $item->detail }}</p>
                    </div>
                @empty
                    <p>登録商品がまだありません。</p>
                @endforelse
            </div>
        </div>
    </body>
</html>

<style>
    .item-container {
        width: 80%;
        margin-left: auto;
        margin-right: auto;
    }
    .item-wrap{
        display: flex;
        flex-wrap:wrap;
        gap:1%;
    }
    .item{
        width: 32%;
    }
    .item img{
        width: 100%;
    }
    .item h2{
        font-size: 18px;
        margin-bottom: 10px;
        text-align: center;
    }
    @media screen and (min-width: 1023px) {
        .item h2 {
            font-size: 25px;
        }
    }
    .item p{
        font-size: 13px;
        margin-top: 5px;
    }
    @media screen and (min-width: 1023px) {
        .item p {
            font-size: 16px;
        }
    }
    h1 {
        font-size: 18px;
        text-align: center;
    }
    @media screen and (min-width: 1023px) {
        h1 {
            font-size: 25px;
        }
    }
</style>

下記の表示になったら正常に表示されています。

20250303_191121_ec6.jpg
下記の画像のパスが難しいかもしれません。

<img src="{{ asset('storage/images/' . $item->image) }}" alt="">

「storage/images」の部分は商品の登録をする時にStockモデルに記述したコードから判断しています。(下記参照)

$request->file('image')->move('storage/images',$name);

次は商品の削除をします。


商品の削除

web.phpに下記の記述をします。

Route::middleware('auth')->group(function () {
    Route::controller(ShopController::class)->group(function () {
        Route::get('/stock/create', 'create')->name('shop.create');

        Route::post('/stock/store', 'store')->name('shop.store');

        Route::get('/index', 'index')->name('shop.index');

        Route::delete('/stock/{stock}/delete', 'delete')->name('shop.delete');
    });
});

Stockモデルに下記の記述をします。

public function itemDelete() {
    $this->delete();
}

Shopコントローラーに下記の記述をします。

public function delete(Stock $stock) {
    $stock->itemDelete();

    return back()->with('message', '商品を削除しました');
}

shop/index.blade.phpに追記します。

<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>商品一覧</title>
    </head>
    <body>
        <div class="item-container">
            <h1>商品一覧</h1>


            //ここから追加
            @if(session('message'))
                <div class="flash">
                    {{ session('message') }}
                </div>
            @endif
            //ここまで追加


            <div class="item-wrap">
                @forelse($items as $item)
                    <div class="item">
                        <h2>{{ $item->name }}</h2>
                        <img src="{{ asset('storage/images/' . $item->image) }}" alt="">
                        <p>{{ $item->detail }}</p>


                        //ここから追加
                        <form method="post" action="{{ route('shop.delete', ['stock' => $item->id]) }}">
                            @csrf
                            @method('DELETE')
                            <button class="delete-button" onClick="return confirm('本当に削除しますか?');">削除</button>
                        </form>
                        //ここまで追加


                    </div>
                @empty
                    <p>登録商品がまだありません。</p>
                @endforelse
            </div>
        </div>
    </body>
</html>

<style>
    .item-container {
        width: 80%;
        margin-left: auto;
        margin-right: auto;
    }
    .item-wrap{
        display: flex;
        flex-wrap:wrap;
        gap:1%;
    }
    .item{
        width: 32%;
    }
    .item img{
        width: 100%;
    }
    .item h2{
        font-size: 18px;
        margin-bottom: 10px;
        text-align: center;
    }
    @media screen and (min-width: 1023px) {
        .item h2 {
            font-size: 25px;
        }
    }
    .item p{
        font-size: 13px;
        margin-top: 5px;
    }
    @media screen and (min-width: 1023px) {
        .item p {
            font-size: 16px;
        }
    }
    h1 {
        font-size: 18px;
        text-align: center;
    }
    @media screen and (min-width: 1023px) {
        h1 {
            font-size: 25px;
        }
    }


    //ここから追加
    .delete-button{
        font-size: 13px;
    }
    .flash {
        font-size: 13px;
        border: 1px solid red;
        padding: 10px;
        text-align: center;
    }
    @media screen and (min-width: 1023px) {
        .flash {
            font-size: 16px;
        }
    }
    //ここまで追加

    
</style>

これで削除をしたら下記の表示になります。

20250304_094142_ec7.jpg

最後は商品の編集です。


商品の編集

web.phpに下記の記述をします。

Route::middleware('auth')->group(function () {
    Route::controller(ShopController::class)->group(function () {
        Route::get('/stock/create', 'create')->name('shop.create');

        Route::post('/stock/store', 'store')->name('shop.store');

        Route::get('/index', 'index')->name('shop.index');

        Route::delete('/stock/{stock}/delete', 'delete')->name('shop.delete');


        //ここから追加
        Route::get('/stock/{stock}/edit', 'edit')->name('shop.edit');

        Route::put('/stock/{stock}/update', 'update')->name('shop.update');
        //ここまで追加


    });
});

Stockモデルに下記の記述をします。

public function itemUpdate($request) {
    $inputs = $request->validate([
        'name' => 'required',
        'detail' => 'required',
        'price' => 'required',
        'image' => 'required',
    ]);

    $this->name = $inputs['name'];
    $this->detail = $inputs['detail'];
    $this->price = $inputs['price'];
    $original_name = $request->file('image')->getClientOriginalName();
    $name = date('Ymd_His').'_'.$original_name;
    $request->file('image')->move('storage/images',$name);
    $this->image = $name;
    $this->update();
}

Shopコントローラーに下記の記述をします。

public function edit(Stock $stock) {
    return view('shop.edit', compact('stock'));
}

public function update(Stock $stock, Request $request) {
    $stock->itemUpdate($request);

    return back()->with('message', '商品情報の編集をしました');
}

「shop/index.blade.php」に編集ページのリンクを付けます。

<div class="item-wrap">
    @forelse($items as $item)
        <div class="item">
            <h2>{{ $item->name }}</h2>
            <img src="{{ asset('storage/images/' . $item->image) }}" alt="">
            <p>{{ $item->detail }}</p>
            <form method="post" action="{{ route('shop.delete', ['stock' => $item->id]) }}">
                @csrf
                @method('DELETE')
                <button class="delete-button" onClick="return confirm('本当に削除しますか?');">削除</button>
            </form>
            <a class="item-edit" href="{{ route('shop.edit', ['stock' => $item->id]) }}">編集</a>         //この行を追加
        </div>
    @empty
        <p>登録商品がまだありません。</p>
    @endforelse
</div>


//cssの追加
.item-edit{
    font-size: 13px;
    margin-top: 10px;
    display: inline-block;
}

現状誰でも編集ページに移動できますが管理者だけが編集ページに移動できるようにするのはecサイト作成の別の記事で解説します。

shopフォルダの下にedit.blade.phpを作成して下記の記述をします。

<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>商品編集ページ</title>
    </head>
    <body>
        <div class="product-edit-container">
            @if(session('message'))
                <div class="flash">
                    {{ session('message') }}
                </div>
            @endif
            @if($errors->any())
            <ul class="error">
                @foreach($errors->all() as $error)
                    <li>{{$error}}</li>
                @endforeach
            </ul>
            @endif
            <h1 class="item-name">商品編集</h1>
            <form action="{{ route('shop.update', ['stock' => $stock->id]) }}" method="post" enctype="multipart/form-data">
                @csrf
                @method('put')
                <div class="title-wrap">商品タイトル : <input type="text" name="name" placeholder="商品タイトル" value="{{ old('name', $stock->name) }}"></div>
                <div class="image-wrap">商品画像 : <input type="file" name="image" accept="image/jpg,image/png"></div>
                <img src="{{ asset('storage/images/' . $stock->image) }}" alt="">
                <div class="detail-wrap">商品説明 : <textarea type="text" name="detail" cols="40" rows="10" placeholder="商品説明">{{ old('detail', $stock->detail) }}</textarea></div>
                <div class="price-wrap">値段 : <input type="text" name="price" placeholder="10000" value="{{ old('price', $stock->price) }}"></div>
                <div class="submit-wrap"><input type="submit" value="更新"></div>
            </form>
        </div>
    </body>
</html>

<style>
    .product-edit-container {
        width: 80%;
        margin-left: auto;
        margin-right: auto;
    }

    .item-name {
        font-size: 18px;
        text-align: center;
    }
    @media screen and (min-width: 1023px) {
        .item-name {
            font-size: 25px;
        }
    }

    .title-wrap, .image-wrap, .detail-wrap, .price-wrap {
        font-size: 13px;
        margin-bottom: 20px;
    }
    @media screen and (min-width: 1023px) {
        .title-wrap, .image-wrap, .detail-wrap, .price-wrap {
            font-size: 16px;
        }
    }
    .flash {
        font-size: 13px;
        border: 1px solid red;
        padding: 10px;
        text-align: center;
    }
    @media screen and (min-width: 1023px) {
        .flash {
            font-size: 16px;
        }
    }
    img{
        width: 100%;
    }
    @media screen and (min-width: 1023px) {
        img{
            width: 40%;
            display: block;
            margin-bottom: 20px;
        }
    }
</style>

これで編集ができるようになりますが欠点として今のままでは編集するたびに画像も新しく選択しないといけません。

編集前の画像を使う方法はおまけで解説します。

今回はここまでです。

今までのアプリ作りの記事を何度もやった人にとって今回の解説は新しい内容があまりなかったはずです。

だから今回の解説がイマイチ理解できない人は今までのアプリ作りの記事を何度もやって自分の中でコードの書き方を定着させてからこの記事の内容を再び読むとすっと頭に入ると思います。

最後におまけの解説です。


おまけ1 : 商品登録後に編集ページにリダイレクトする

Stockモデルに下記の追記をします。

public function itemStore($request) {
    $inputs = $request->validate([
        'name' => 'required',
        'detail' => 'required',
        'price' => 'required',
        'image' => 'required',
    ]);

    $this->name = $inputs['name'];
    $this->detail = $inputs['detail'];
    $this->price = $inputs['price'];
    $original_name = $request->file('image')->getClientOriginalName();
    $name = date('Ymd_His').'_'.$original_name;
    $request->file('image')->move('storage/images',$name);
    $this->image = $name;
    $this->save();
    return $this;             //この行を追加
}

最後の行を書くことで商品情報をコントローラーに渡します。

Shopコントローラーのstoreアクションを修正します。

public function store(Stock $stock, Request $request) {


    //ここから修正
    $created_stock = $stock->itemStore($request);

    return redirect()->route('shop.edit', ['stock' => $created_stock->id])->with('message','商品の投稿が完了しました');
    //ここまで修正

    
}

「$created_stock」に登録した商品情報が入っています。

試しに「dd($created_stock);」で下記の表示になります。

App\Models\Stock {#1302// app/Http/Controllers/ShopController.php:19
  #connection: "mysql"
  #table: null
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: []
  #withCount: []
  +preventsLazyLoading: false
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: true
  #escapeWhenCastingToString: false
  #attributes: array:7 ["name" => "デモタイトル"
    "detail" => "デモ商品説明"
    "price" => "100"
    "image" => "20250304_024455_1.jpg"
    "updated_at" => "2025-03-04 02:44:55"
    "created_at" => "2025-03-04 02:44:55"
    "id" => 18
  ]
  #original: array:7 []
  #changes: []
  #casts: []
  #classCastCache: []
  #attributeCastCache: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: []
  #touches: []
  +timestamps: true
  +usesUniqueIds: false
  #hidden: []
  #visible: []
  #fillable: array:4 []
  #guarded: array:1 []
}

「['stock ' => $created_stock->id]」のstockはweb.phpの編集のURLの設定を見ています。(下記参照)

Route::get('/stock/{stock}/edit', 'edit')->name('shop.edit');

これで商品情報の登録をすると編集画面にリダイレクトします。


おまけ2 : 編集時に元画像を使う

StockモデルのitemUpdateメソッドを修正します。

public function itemUpdate($request) {
    $inputs = $request->validate([
        'name' => 'required',
        'detail' => 'required',
        'price' => 'required',
        'image' => 'nullable',             //この行を修正
    ]);

    $this->name = $inputs['name'];
    $this->detail = $inputs['detail'];
    $this->price = $inputs['price'];

    if ($request->hasFile('image')) {        //この行を追加
        $original_name = $request->file('image')->getClientOriginalName();
        $name = date('Ymd_His').'_'.$original_name;
        $request->file('image')->move('storage/images',$name);
        $this->image = $name;
    }                     //この行を追加
    
    $this->update();
}

画像の追加を必須にしていたのを必須でなくします。(下記参照)

'image' => 'nullable'

そして新たに登録した画像がある場合の画像の更新をするようにします。

if文の所です。

hasFileメソッドで画像を登録したかを判定できます。


次の解説

下記のリンクからどうぞ。

https://laravelyarubai.newsite-make.com/post/32/view










戻る