#はじめに
##なぜオークションサイトを作ろうと思ったのか
単純に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 | 内容     | 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内にシンボリックリンクが自動表記されますので適宜更新してください。以下一例
'links' => [
//出品情報用
public_path('storage') => storage_path('app/public'),
//マイページ自画像用
public_path('mypage') => storage_path('app/public'),
],
コントローラーは以下のように。本来fileデータ取得時はUploadedFileクラスのextensionメソッド、pathメソッドを使う方がスマートだと思います。
しかし私の力不足でうまく起動せず。時間がなかったのでphpの原型であるpathinfoを使って画像ファイルの名前と拡張子を取得しました。
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!'));
}
テンプレートではこのように画像リンクを貼ります。
<img src="{{ asset('images/images/'.$prof_info->prof_img) }}" alt="me!">
###会員削除
削除すること自体は大して難しくはありません。ただ物理削除ではなく論理削除する場合は一部設定に修正が必要です。
migrationする際にusersテーブルにdeleted_atを追加します。
public function up()
{
Schema::create('users', function (Blueprint $table) {
//追加
$table->softDeletes('deleted_at',0);
});
モデルも追加する必要があります。
class User extends Authenticatable{
use SoftDeletes;
//softdelete用
protected $table = 'users';
protected $dates = ['deleted_at'];
//以下略
}
あとはコントローラーでdeleteメソッド使えば論理削除されます。
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にしていたおかげで更新されず時間をロスしてしまいました。ちゃんと型は把握しておくべきだと心に刻みました。
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
<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メールを使います。
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
メーラー等メール関係の設定を変更する必要があります。
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@管理人"
お問い合わせでは返信の可否を問いていますので、それに対する処理する必要があります。これはモデルで行います。
class Contacts extends Model
{
protected $table = 'Contacts';
protected $fillable = [
'name','comment','reply','email','subject'
];
//返信可否
static $reply = [
'必要','不要'
];
}
コントローラー側ではMailクラスのtoとsendメソッドを使って送信処理を行います。
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でもよかったなと少し反省しています。
<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を設定しています。
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と設定しています。
<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を使用。
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の変更
ここはバグると怖かったのでバックアップを取りながら修正しました。
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の変更
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(管理者側)で以下のように分けています。
//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先が異なりますのでそれぞれ変更を行います。
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)の変更
上記で変更した設定をコントローラー側に反映します。以下機械的に変更するだけですので説明は省きます。
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にも追記する必要があります。
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)
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)
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)
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
他確認次第追記予定