2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Laravel7.x系でオークションサイトを作ってみた時の備忘録

Last updated at Posted at 2020-08-13

#はじめに

##なぜオークションサイトを作ろうと思ったのか
単純にLaravel学習のアウトプットとして、CRUDのいい復習になると思ったからです。
なおLaravel部分の作業が一旦完了したためこの記事を作成しましたが、完成した暁には残りの作業分も追記もしくは新規記事にできたらなと思います。

##事前準備

オークションサイトと言っても決済の部分は省略し、ジモティーのように物々交換する仕様。お互い指定の場所で指定の商品を譲り合うという前提で成立。

###使用言語など

  • PhpStorm使用
  • Composer version 1.10.9
  • Laravel Framework 7.21.0 →今回記述分
  • JavaScript・jQuery(のちに追記or新規作成予定)
  • Vue(のちに追記or新規作成予定)

###実装一覧

実装した機能をまとめました。

No 機能名 備考
1 会員登録・変更・削除
2 ログイン・ログアウト
3 マイページ プロフィール画像更新実装
4 管理人登録・変更・削除 マルチログイン仕様
5 管理人権限で全ての情報を登録・変更・削除可能 管理人室の機能
6 出品情報登録・変更・削除
7 出品情報検索 あいまい検索、カテゴリ別検索も可能
8 落札・落札後のメッセージ作成・取引完了 落札後の実装
9 会員登録や出品登録時の画像アップロード 出品時に登録可能な画像は5枚まで
10 パスワードリセット
11 パスワード再発行
12 お問い合わせ

###URI一覧
作成したURI一覧です。管理人画面分は除きます。

No 内容 &nbsp&nbsp&nbsp URI アクション Name
1 TOPページ / HomeController@index index
2 会員登録(get) /register Auth\RegisterController@showRegistrationForm showRegister
3 会員登録(post) /register Auth\RegisterController@register register
4 ログイン(get) /login Auth\LoginController@showLoginForm showLogin
5 ログイン(post) /login AUth\LoginController@login login
6 検索結果表示(get) /search/result HomeController@search search
7 ログアウト(post) /logout Auth\LoginController@logout logout
8 出品情報登録(get) /member/trade/register Member\ProductsController@new tradeRegister
9 出品情報登録(post) /member/trade/register Member\ProductsController@create tradeCreate
10 登録済出品情報一覧(get) /member/trade/list Member\ProductsController@list tradeList
11 出品情報変更(get) /member/trade/{id}/edit Member\ProductsController@edit tradeEdit
12 出品情報変更(post) /member/trade/{id}/update Member\ProductsController@update tradeUpdate
13 出品情報画像変更(get) /member_trade/{id}/editImages Member\ProductsController@editImage editImage
14 出品情報画像変更(post) /member_trade/{id}/editUploads Member\ProductsController@editUploads editUploads
15 マイページ(get) /member/mypage Member\MypageController@index mypage
16 マイページ(post) /member/mypage Member\MypageController@upload profUpload
17 会員情報修正(get) /member/register/edit Member\MypageController@edit memberEdit
18 会員情報修正(post) /member/register/update Member\MypageController@update memberUpdate
19 出品情報削除(get) /member/remove/{id} Member\ProductsController@remove tradeRemove
20 出品情報削除(post) /member/delete/{id} Member\ProductsController@delete tradeDelete
21 会員登録削除プレビュー(get) /member/delete_preview Member\MypageController@preview deletePreview
22 会員登録削除(post) /member/delete Member\MypageController@delete deleteMember
23 落札リスト(get) /member/bid/list Member\BidsController@nowBidList member.bid.list
24 落札後チャット画面(get) /member/bid/{id} BidsController@showBidItem member.bid
25 落札後チャット画面(post) /member/bid/detail/{id} Member\BidsController@BidBoard member.BidBoard
26 落札実装(post) /trade/change/{id} Member\BidsController@bid bidUpdate
27 落札された一覧(get) /member/trade/tradedList Member\BidsController@productsList member.traded.list
28 取引終了(get) /member/bid/end/{id} Member\BidsController@bidEnd member.bid.end
29 取引終了(post) /member/end/{id} Member\BidsController@End member.end
30 出品情報詳細(get) /trade/{id} Member\BidsController@simple tradeSimple
31 パスワード再発行(get) /auth/confirm Auth\ConfirmPasswordController@showConfirmForm showConfirmForm
32 パスワード再発行(post) /auth/confirm Auth\ConfirmPasswordController@confirm password.confirm
33 パスワードリセット通知メール(get) /password/sendEmail Auth\ForgotPasswordController@showLinkRequestForm password.email.send
34 パスワードリセット通知メール(post) /password/email Auth\ForgotPasswordController@sendResetLinkEmail password.email
35 パスワードリセット(get) /password/reset/{token} Auth\ResetPasswordController@showResetForm showResetForm
36 パスワードリセット(post) /password/reset Auth\ResetPasswordController@reset password.update
37 お問い合わせ入力(get) /contact ContactController@show contact
38 お問い合わせ入力(post) /contact/confirm ContactController@confirm confirm
39 お問い合わせ受付完了(post) /contact/send ContactController@send send

###Table一覧
作成したTable一覧です。

No テーブル名 forgin_key
1 users なし
2 categories なし
3 products users_id,categories_id
4 bidders bidders_id,trades_id,product_user_id
5 admins なし
6 contacts なし
7 boards products_id
8 messages boards_id,users_id
9 password_resets なし
10 migrations なし

以上で事前情報まとめは完了。以下実装まとめを記載。

##会員登録・変更・削除
特に特徴のある実装内容はないけれど気になった点を書きます。

###認証
Laravel6.x以降ではphp artisan make:authと打っても一発で認証が設定されません。
先にcomposerでlaravel/uiをインストールし、php artisan ui vue --authを実行する必要です。

composer require laravel/ui
php artisan ui vue --auth

参考:https://readouble.com/laravel/7.x/ja/authentication.html

###画像アップロード
意外と悩んだ実装の1つ。おそらくPHP自体を中途半端に理解しているのが原因だと思われます。そのうちPHP自体の復習も行いたいですね。

手順としては一時保存後(file,store)、公開用ディレクトリ(シンボリックリンク)に保存する作業が必要。具体的にはfileでアップロード、storeでstorageディレクトリに一時保存する流れです。
この時点だと公開ディレクトリに画像が入っていないのでシンボリックリンク機能を適用しpublic内にstorageディレクトリを作成、public内のデータへ画像のリンクを貼る必要があります。

当初このシンボリックリンクがエラー頻出し、先に進めず悩んで酒に溺れました。結局シンボリックリンクがエラーを起こしていたわけではなくbladeのデータで画像リンクの表記方法が間違っていたという悲しいオチでしたが。

シンボリックリンクを機能させるにはphp artisan storage:linkを実行します。

php artisan storage:link

そうするとconfig/filesystems.php内にシンボリックリンクが自動表記されますので適宜更新してください。以下一例

config/filesytems.php
    'links' => [
        //出品情報用
        public_path('storage') => storage_path('app/public'),
        //マイページ自画像用
        public_path('mypage') => storage_path('app/public'),
    ],

コントローラーは以下のように。本来fileデータ取得時はUploadedFileクラスのextensionメソッド、pathメソッドを使う方がスマートだと思います。
しかし私の力不足でうまく起動せず。時間がなかったのでphpの原型であるpathinfoを使って画像ファイルの名前と拡張子を取得しました。

MypageController.php
    public function upload(MypageUploadRequest $request){
        $prof_img =User::find(Auth::id());

        //upload
        //一時保存
        $tmpFile = $request->file('prof_img')->store('public/images');
        //一時保存したデータの名前と拡張子を取得
        $filename = pathinfo($tmpFile, PATHINFO_FILENAME);
        $extension = Pathinfo($tmpFile, PATHINFO_EXTENSION);

        //マイページでリンクを貼る時の画像名称
        $uploads_name = $filename . '.' . $extension;
        $prof_img->prof_img = $uploads_name;
        $prof_img->save();

        return redirect('member/mypage')->with('flash_message',__('Uploaded!'));
    }

テンプレートではこのように画像リンクを貼ります。

mypage.blade.php
<img src="{{ asset('images/images/'.$prof_info->prof_img) }}" alt="me!">

###会員削除
削除すること自体は大して難しくはありません。ただ物理削除ではなく論理削除する場合は一部設定に修正が必要です。

migrationする際にusersテーブルにdeleted_atを追加します。

migrations_users_table.php
public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            //追加
            $table->softDeletes('deleted_at',0);
        });

モデルも追加する必要があります。

Models/User.php
class User extends Authenticatable{
    use SoftDeletes;

    //softdelete用
    protected $table = 'users';
    protected $dates = ['deleted_at'];
   
   //以下略
}

あとはコントローラーでdeleteメソッド使えば論理削除されます。

MypageController.php
    public function delete(){
        //softdelete
        User::find(Auth::id())->delete();
        return redirect('/')->with('flash_message',__('Member Deleted.'));
    }

##出品情報登録・変更・削除
会員情報と異なる点は落札時、取引時にフラグを立てる必要があること。
フラグの可否はtrade_flag(落札)、end_flag(取引終了)のカラムを設定。どちらともデフォルト値に0を入れておきます。そして落札時、取引完了時にフラグが1に変更する仕組み。

テーブル名 カラム名
products trade_flag boolean
bidders end_flag boolean

ここで犯したミスはこのフラグカラムをフォームリクエストにうまく反映されず更新されなかったのと、bladeの書き方。booleanの型をずっとstringにしていたおかげで更新されず時間をロスしてしまいました。ちゃんと型は把握しておくべきだと心に刻みました。

ProductsRequest.php
    public function rules()
    {
        return [
            //フラグのバリデーション
            'trade_flag' => 'boolean | max:1',
            //以下略
        ];
    }

checkboxはチェックをしていないとvalueの値をサーバーに送ってくれない仕様なのでchecked="checked"必須。

フォームが送信されたときにチェックボックスがチェックされていなかった場合、チェックされていない状態を表す値 (value=unchecked など) が送信されることはなく、値はサーバーに全く送信されません。
参考:https://developer.mozilla.org/ja/docs/Web/HTML/Element/Input/checkbox

trade_simple.blade.php
<input type="checkbox" name="trade_flag" value="1" checked="checked" hidden>

##お問い合わせ機能
.envとconfig/mail.php内の設定を一部変更する必要があります。

###mail.php

Default Mailerは初期値がSmtpなのでそのままでOK、Mailer ConfigurationsやGlobal "From" Addressは管理者のメール設定に変更します。今回はロリポップのwebメールを使います。

config/mail.php
    return [
       //Default Mailer
       'default' => env('MAIL_MAILER', 'smtp'),

       //Mailer Configurations
       'mailers' => [
        'smtp' => [
            'transport' => 'smtp',
            'host' => env('MAIL_HOST', 'smtp.lolipop.jp'),
            'port' => env('MAIL_PORT', 465),
            'encryption' => env('MAIL_ENCRYPTION', 'ssl'),
            'username' => env('MAIL_USERNAME'),
            'password' => env('MAIL_PASSWORD'),
            'timeout' => null,
            'auth_mode' => null,
        ],

      //Global "From" Address
      'from' => [
        'address' => env('MAIL_FROM_ADDRESS', '****@****.com'),
        'name' => env('MAIL_FROM_NAME', 'Locattey@管理人'),
    ],
    ]

###.env
メーラー等メール関係の設定を変更する必要があります。

.env
MAIL_MAILER=smtp
MAIL_HOST=smtp.lolipop.jp
MAIL_PORT=465
MAIL_USERNAME=****@******.com
MAIL_PASSWORD=*************
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS=****@******.com
MAIL_FROM_NAME="Locattey@管理人"

お問い合わせでは返信の可否を問いていますので、それに対する処理する必要があります。これはモデルで行います。

Models/Contacts.php
class Contacts extends Model
{
    protected $table = 'Contacts';

    protected $fillable = [
        'name','comment','reply','email','subject'
    ];
    
    //返信可否
    static $reply = [
        '必要','不要'
    ];
}

コントローラー側ではMailクラスのtoとsendメソッドを使って送信処理を行います。

ContactsController.php
    public function show(){
        //返信可否のデータを表示
        $reply = Contacts::$reply;
        return view('contact',compact('reply'));
    }

    public function confirm(ContactRequest $request){
            
        $contact = new Contacts($request->all());
        $contact->fill($request->all())->save();
            
        //送信
        Mail::to($contact->email)->send(new ContactSendmail($contact));
        return view('contact_confirm',compact('reply','contact'));
    }

view側で返信可否のリクエストはラジオボタンで取得しています。今思えばcheckboxでもよかったなと少し反省しています。

contact.blade.php
   <form class="mail_form" method="POST" action="{{ route('confirm') }}">
     @csrf
     <label class="label">
     <span class="label_item">返信が必要ですか?:</span>
     @foreach($reply as $item)
      <input type="radio" name="reply" value="{{ $item }}" >{{ $item }}
     @endforeach
     </label>
     <input type="submit" name="action" class="submit_send" value="確認">
   </form>

##パスワードリセット
パスワード再発行がリクエストされたらメールアドレスでパスワードリセット用のURLを通知するよう実装します。
まずは通知設定を行うために通知の作成を行います。make:notificationを実行しましょう。
(先のお問い合わせの時点で.envとmail.phpの設定は済んでいるため、2つのファイルはスルーでOK)

php artisan make:notification PasswordResetNotification

作成されたPasswordResetNotificationを一部更新します。
今回はHTML形式でメールを送ることにしたのでviewを設定しています。

PasswordResetNotification.php
class PasswordResetNotification extends Notification
{
    use Queueable;

    public $token;
    protected $title = 'パスワードリセット 通知';

    //省略

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject($this->title)
            ->view('auth.passwords.passwordreset',
                [
                    'reset_url' => url('password/reset', $this->token),
                ]);
    }
   //省略
}

パスワードリセット先のURLは$reset_urlと設定しています。

passwordreset.blade.php
    <body>
        <div class="section">
            <h1>パスワードリセットを行いますか</h1>
            <p>パスワードリセットを行いたい場合はリンクパスワードリセットをクリックしてパスワードを再設定してください</p>
            <p class="link"><a href="{{ $reset_url }}">パスワードリセット</a></p>
        </div>
        <div class="from">
            Locattey@管理人より( info@yukarisite.com )
        </div>
    </body>

##検索実装

Eloquentの書き方の問題で少し戸惑った部分があったため記事に残しておきます。
検索方法は3種類。カテゴリ検索・キーワード検索・カテゴリ+キーワード検索。selectして表示するだけでいいのでpost分のメソッドは不要。

###検索文字、カテゴリ選択可否の情報取得
検索文字は入力フォーム入力してもらい、カテゴリはカテゴリ選択しそれぞれ値を取得。
あいまい検索をしたいので%{$keyword}%とLIKE、OR検索したい場合はorwhereを使用。

HomeController.php
    public function search(Request $request){
        $keyword = $request->search_word;
        $keyword_category = $request->search_categories;

        if(!empty($keyword_category) and empty($keyword)){
            //カテゴリ選択のみ
            $results = Products::where('categories_id','like',"%{$keyword_category}%")
                ->where('trade_flag',0)
                ->get();

        }elseif(empty($keyword_category) and !empty($keyword)){
            //検索キーワード入力のみ
            $results = Products::where('trade_flag',0)
                 ->where('product_name','like',"%{$keyword}%")
                ->orwhere('product_describe','like',"%{$keyword}%")
                ->get();

        }elseif(!empty($keyword_category) and !empty($keyword)){
            //カテゴリ選択あり、検索キーワード入力あり
            $results = Products::where('trade_flag',0)
                ->where('categories_id','like',"%{$keyword_category}%")
                ->where('product_name','like',"%{$keyword}%")
                ->where('product_describe','like',"%{$keyword}%")
                ->get();
        }else{
            //検索ボタン押しただけ
            $results = Products::where('trade_flag','=',0)->orderBy('id','DESC')->paginate(10);
        }

##管理者用ログイン
いわゆるマルチ認証の実装。ここが一番つまづいたかもしれません。ネットにはLaravel7.x系の情報が少なかったのでいざって時は困りました。

手順としてはapp/Http/controllers/Authの中身全てをコピー→Controller内で適当にディレクトリを用意してその中にペーストして作業開始。私の場合はapp/Http/Controllers/Admin/Authとしました。
本当はusersの方のAuthもAdminディレクトリのように一つ上の階層を作った方が見栄えが良くていいのですが設定しなおすのが面倒くさいので今回はやめました。以下構成図。

├── Http
│   ├── Controllers
│   │   ├── Admin
│   │   │   ├── Auth
│   │   │   │   ├── ConfirmPasswordController.php
│   │   │   │   ├── ForgotPasswordController.php
│   │   │   │   ├── LoginController.php
│   │   │   │   ├── RegisterController.php
│   │   │   │   ├── ResetPasswordController.php
│   │   │   │   └── VerificationController.php
│   │   │   ├── HomeController.php
│   │   │   └── ManagerController.php
│   │   ├── Auth
│   │   │   ├── ConfirmPasswordController.php
│   │   │   ├── ForgotPasswordController.php
│   │   │   ├── LoginController.php
│   │   │   ├── RegisterController.php
│   │   │   ├── ResetPasswordController.php
│   │   │   └── VerificationController.php

Auth系の設定の面倒臭さってtraitやuse使ってるから元ネタどっちかわからなくなる点。設定するだけなら便利ですが修正含めると不便で好きになれません。
というわけで以下設定変更する箇所をピックアップ。

###Models/Admins.phpの作成
php artisan make:model Models/Adminsで作成したらUser.phpの中身をコピペして終わり。

###config/auth.phpの変更
ここはバグると怖かったのでバックアップを取りながら修正しました。

config/auth.php
return [
//Authentication Defaults
    'defaults' => [
        'guard' => 'user',
        'passwords' => 'users',
    ],

//Authentication Guards
    'guards' => [
        'user' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],
        //追記
        'admin'=>[
            'driver' => 'session',
            'provider'=> 'admins'
        ]
    ],

//User Providers
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
            'table' => 'password_resets',
            'expire' => 30,
        ],
        //追記
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
            ]
    ],

//Resetting Passwords
    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
        //追記
        'admins'=>[
            'provider' => 'admins',
            'table' => 'password_resets',
            'expire' => 60
        ]
    ],

];

##Middleware関係修正
redirect先の設定が一括にまとめられているなどlaravel6.x以降では根本的な構造が変わっているため古い情報をアテにしすぎると詰みます(経験談)。

###Authenticate.phpの変更

Middleware/Authenticate.php
class Authenticate extends Middleware
{
    //redirectの設定
    //admin用
    protected $admin_route = 'admin.login';
    //user用
    protected $user_route = 'login';

    protected function redirectTo($request)
    {
        if (! $request->expectsJson()) {
            if(Route::is('auth')){
                return route('login');
            }elseif(Route::is('auth:admin')){
                return route('admin.login');
            }
        }
    }
}

###route.phpの変更
ちなみにrouteの設定はusers(ユーザー側)とauth(管理者側)で以下のように分けています。

route.php
//user側
Route::group(['middleware' => 'auth'],function(){
  //user側のroute設定
  Route::post('/logout','Auth\LoginController@logout')->name('logout');
  //etc...
}

//auth側
Route::group(['prefix'=>'admin','middleware'=>'auth:admin'],function(){
  //auth側のroute設定
  Route::get('/home','Admin\HomeController@index')->name('admin.home');
  //etc...
}

話を戻して残りのMiddleware変更箇所をリストアップします。

###RedirectIfAuthenticated.phpの変更
ユーザー側と管理者側ではredirect先が異なりますのでそれぞれ変更を行います。

Middleware/RedirectIfAuthenticated.php
class RedirectIfAuthenticated
{
    public function handle($request, Closure $next, $guard = null)
    {
        //ユーザー側のredirect
        if (Auth::guard($guard)->check() && $guard === 'user') {
            return redirect(RouteServiceProvider::HOME);

        //管理者側のredirect
        }elseif(Auth::guard($guard)->check() && $guard === 'admin') {
            return redirect(RouteServiceProvider::ADMIN_HOME);
        }

        return $next($request);
    }
}

###LoginController.php(Admin)の変更
上記で変更した設定をコントローラー側に反映します。以下機械的に変更するだけですので説明は省きます。

LoginController.phpの修正(Admin)
class LoginController extends Controller
{
    use AuthenticatesUsers;

    //Admin用に変更
    protected $redirectTo = RouteServiceProvider::ADMIN_HOME;

    protected function guard()
    {
    //Admin用に変更
        return Auth::guard('admin');
    }

    public function __construct()
    {
    //Admin用に変更
        $this->middleware('guest:admin')->except('logout');
    }
    public function showLoginForm()
    {
    //Admin用に変更
        return view('admin.auth.login');
    }

    public function logout(Request $request)
    {
    //Admin用に変更
        Auth::guard('admin')->logout();

        return $this->loggedOut($request);
    }

    public function loggedOut(Request $request)
    {
    //Admin用に変更
        return redirect(route('admin.login'));
    }
}

###RegisterController.phpの変更(Admin)
use先のRegistersUsersにも追記する必要があります。

RegisterController.php(admin)
class RegisterController extends Controller
{
    use RegistersUsers;

    public function __construct()
    {
    //Admin用に変更
        $this->middleware('guest:admin');
    }

    protected function guard()
    {
    //Admin用に変更
        return Auth::guard('admin');
    }

    public function showRegistrationForm()
    {
        return view('admin.auth.register');
    }
    //以下略
}

###RegistersUsers.phpの修正(Admin)

RegistersUsers.php(Admin)
trait RegistersUsers
{
    use RedirectsUsers;

    //user用
    public function showRegistrationForm()
    {
     //   return view('auth.register');
        return view('register');
    }

    //admin用
    public function showRegistrationFormAdmin(){
        return view('admin.register');
    }
}

ここまででAdmin用の修正は完了。これと同じ作業をUser側でも行います。

###LoginController.phpの修正(user)

LoginController.php(user)
class LoginController extends Controller
{
    use AuthenticatesUsers;

    //user側のredirect
    protected $redirectTo = RouteServiceProvider::HOME;

    public function __construct()
    {
        //要確認&修正箇所
        $this->middleware('guest:user')->except('logout');
    }

    //guard
    protected function guard()
    {

        //要確認&修正箇所
        return Auth::guard('user');
    }

    //ログイン画面
    //user ver
    public function showLoginForm()
    {
        //要確認&修正箇所
//        return view('auth.login');
        return view('login');
    }
}

###RegisterController.phpの修正(user)

RegisterController.php(user)
class RegisterController extends Controller
{
    use RegistersUsers;

    public function __construct()
    {
        //要確認&修正箇所
        $this->middleware('guest:user');
    }

    //guard
    public function guard()
    {
        //要確認&修正箇所
            return Auth::guard('user');
    }

    //view
    //user
    //要確認&修正箇所
    public function showRegistrationForm()
    {
        //   return view('auth.register');
        return view('register');
    }
    //以下略
}

管理者の方では通知やパスワード再設定の実装は省略しました。
他にもいくつかありますが小ネタなので別記事に載せます。

至らぬ点は多いと思いますが根気よく読んでくださりありがとうございました。

参考:
https://readouble.com/
https://qiita.com/sutara79/items/0ea48847f5565aacceea
https://qiita.com/daisu_yamazaki/items/a914a16ca1640334d7a5
https://qiita.com/namizatop/items/5d56d96d4c255a0e3a87
他確認次第追記予定

2
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?