20210220のPHPに関する記事は11件です。

PHP5上級試験/準上級試験の上級合格に挑戦(7) 5章配列前編

前回まで

  • 3章文字列・4章関数と、ここまでやってきたがここまでは前菜
  • 4章関数後編は、結局自分でわかっているものはコードを書くのを省いた。時間があれば追加編集する
  • いよいよ5章から難しくなってくるし、出題率もここから上がっていくので兜を締め直してがんばる

参考

PHP5技術者認定試験(上級)に学ぶ、実は知らなかった配列関数(3) | @tmak_tsukamotoさん

5章配列

配列の基本

※ ここ(上級)まで来た人なら、作り方ぐらいは知ってると思うが、ちゃんとした基本ルールを知って作ってるのと知らないで作ってるのとでは大違いなので、きちんと学ぶこと(自省もこめてここに記す)

  • 配列はキーと値がペアで集まったもの
  • ペアは合わせて要素と呼ぶ
  • 配列作成方法は1)array()構文、(2)角括弧構文の2つ
  • 配列にはインデックス配列と連想配列の2つがある。
  • インデックス配列はキーが0から始まるもので、連想配列はキーが文字列になる。
  • 以下はインデックス配列で、配列作成方法を説明する。

array構文

  • array構文の場合、キーは0から始まる連番が振られる
  • array(キー => 値)と明示的に記述することもできる。この場合、途中に改行を入れたほうが読みやすい
  • 上のキーの指定は途中で省略してOK.その場合、その前の指定したキー+1したキー番号が振られる

以下は例

index.php
//array構文の基本の書き方
 array('a','b','c'); // 出力:0=>'a',1=>'b',2=>'c'
//array構文で明示的に記述。改行入れて読みやすくする
 array(1 => 'a',
       2 => 'b',
       4 => 'c'); // 出力:1=>'a',2=>'b',4=>'c'
//途中でキーの指定を途中から省略
 array(10 => 'a','b','c'); // 出力:10=>'a',11=>'b',12=>'c' 

角括弧([])構文

  • 1行目の代入文の実行で新しい配列が作成され、下の例の場合変数$aに格納
  • 2行目・3行目の代入文で配列の末尾にキーと値が追加
  • キーはarrayと同じく0からはじまる整数が振られる
index.php
//角括弧構文の基本の書き方
 $a[] = 'a';
 $a[] = 'b';
 $a[] = 'c'; // 出力:0=>'a',1=>'b',2=>'c'
  • 配列のキーと値は内部的な並び順を持つ。つまり、キーの順番ではなく、追加された順に並ぶ。 これを利用した問題が出題されたのを確認。しっかり理解すること。
index.php
//角括弧構文の基本の書き方
 $b[3] = 'A';
 $b[2] = 'B';
 $b[1] = 'C'; // 出力:3=>'A',2=>'B',1=>'C'

→上を見ると、キーの順番に並んでおらず、ましてやキーが0に対応する値がない。
 つまり追加順に並ぶということが証明されている。

連想配列

いきなり問題。

配列の出力が 'php' => 1, 'HTML'=> 2, 'CSS' => 3, 'JS' => 4 となるように
array構文と、角括弧構文と2通りの書き方で配列$aを記述せよ。
  
 
 

index.php
//array構文
 $a = array('php' => 1,
            'HTML' => 2,
            'CSS' => 3,
            'JS' => 4);

//角括弧構文
 $a['php'] = 1;
 $a['HTML'] = 2;
 $a['CSS'] = 3;
 $a['JS'] = 4;
  • 多次元配列も作れる。
index.php
 $a = array(
         array(1,2,3),
         array(4,5,6)
        );

// 出力
// 0 =>(
//     0 => 1;
//     1 => 2;
//     2 => 3;
//   )
//   1 => (
//     0 => 4;
//     1 => 5;
//     2 => 6;  
//   )

気をつけるべきルール

  • たとえば array('php'=> 3,'php'=> 4)のように重複したキーを持つ配列は作れない。むしろ、この場合は、'php'キーに対応する値が更新されて4になるのみ。
  • 配列のキーは文字列または整数を指定できるが、オブジェクトは指定できない
  • 浮動小数点数をキーに指定した場合は、小数部分が切り捨てになる。(たとえば1.5だったら1、2.53だったら2)
  • 整数として変換可能な文字列を指定した場合整数に変換されたものがキーに。現実としてありえない数値(10進数でないなど)を指定した場合変換は行われず、文字列としてキーに指定される。(たとえば'134'だったら134というキーに、’0123’だったら10進数としてはありえないので’0123’という文字列として指定される)
  • 連想配列に角括弧構文で値を追加した場合、連想配列に数値キーがない場合0から始まる数値がキーとして追加されそこに値が格納される。もし数値キーがあれば、それに+1した数値がキーとなる。

さて、上記を踏まえた上で、下記の出力(print_r)はどうなるか記述せよ。

index.php
 $a = array(
      'php' => 1,
      'HTML' => 2);
  $a [] = 'css';
  $a [] = 'js';
  print_r($a);

 $b = array(
      'php' => 1,
      'HTML' => 2,
       1 => 3);
  $b [] = 'css';
  $b [] = 'js';
  print_r($b);

 $c = array(
       1 => 3,
      'php' => 1,
      'HTML' => 2);
  $c [] = 'css';
  $c [] = 'js';
  print_r($c);


出力は

Array($a) ( [php] => 1
     [HTML] => 2
     [0] => css
      [1] => js )

Array($b) ( [php] => 1
     [HTML] => 2
     [1] => 3
      [2] => css
      [3] => js )

Array($c) ( [1] => 3
     [php] => 1
     [HTML] => 2
     [2] => css
     [3] => js )

となる。

記事の裏側

そういえば上記の問題を作るときに配列のキーの指定で最初から指定しようと思って、うっかり1から始めてしまってたが、本当は0から。
そのため出力が想定したのと違う結果になったが(笑)、そもそもルールとして、指定されたキーに+1したものが次のキーになるということさえわかってればこの結果も納得行くと思う。
自分でコード作ってテストしてみると想定と違った結果が出ても、理由がわかったりするとおもしろい。バグ検出の練習になると思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel5.8 画像アップロード機能を仕組みから理解する

1.はじめに

ECサイトを開発中に画像アップロード機能について最高に詰まったのでこの記事にまとめます。
誰かのお役に立てたら幸いです。
なお、今回実装したのは
画像をストレージに保存してDBにその「パス」を保存する方法です。

対象読者
・画像アップロードに躓いている方
・Laravel初学者

注)
本記事に記載してあるコードは要点部分のみです。
本記事は画像アップロードの仕組みを理解するためのものとお考えください。

環境
・Laravel Framework 5.8.38
・PHP 7.2.34
・mysql Ver 14.14 Distrib 5.7.32
・phpmyadmin

2.実現したい内容

今回実装したかったのはECサイトにおける商品登録の段階で、
①フォームに画像データを添付して送信したい
②送ったデータをDBに保存したい
③保存された画像データをView(商品一覧)に表示させたい
という内容を実装したい。

つまりは
スクリーンショット 2021-02-20 15.24.13.png
このようなFormを作って、画像添付して登録ボタンを押すと

スクリーンショット 2021-02-20 15.29.15.png
データがデータベースに保存されるようにして

スクリーンショット 2021-02-20 15.31.22.png
保存したものをviewに表示させる。

この一連の流れを習得できるようにします。

3.体系的に理解する

まず前提知識を列挙していきます。

3.1.今回の画像アップロード機能概要

第一に、画像アップロード機能の構造的解釈で躓いたので記載しておきます。
「2.実現したい内容」で述べた手順(構成)がそもそも間違っていました。

実現したい内容を要約すると

①商品登録をする際に、商品情報を記載するフォーム画面に画像を添付して送信する
②その画像をDBに保存する
③商品一覧画面にその画像を表示させる

となりますが、正しくは下記の通りです

①商品登録をする際に、商品情報を記載するフォーム画面に画像を添付して送信する
②画像データ自体はサーバ(Laravel)に保存される
③DBには②で保存した位置情報(ファイルパス)のみを保存する
④商品一覧画面にその画像を表示させる

僕はこの赤文字部分を理解するのに時間がかかりました...
順に解説しますと
「①」はそのままです。

「②、③」に関して、「①」で送信したリクエストに画像データファイルパスの二つの情報を持たせます。そのうち画像データはサーバー(Laravel)内に保存します。ファイルパスのみをDBに保存します。

「④」は表示のさせ方が少しややこしいですが、すぐ理解できると思います。(後ほど記載します)

次に画像データはどこに保存されるか?について記載していきます

3.2.publicフォルダについて

前項で、画像データはサーバー(Laravel)内に保存すると書きましたが
その保存先が、publicディレクトリです。

実際のpublicディレクトリを見てみると...
スクリーンショット 2021-02-20 16.53.44.png

...!?
publicが二つある..!?

ここも理解に苦しみました。。解説します

Laravelにはpublicディレクトリが二つあります。
・public(appと同じ階層)(便宜上、「上のpublic」と呼ばせていただきます)
・storage/app/public   (便宜上、「下のpublic」と呼ばせていただきます)

3.2.1.上のpublicディレクトリ

アプリケーションに送られる、全てのリクエストのエントリーポイント(最初に実行される)となるindex.php ファイルがあるところ。
ここには他に画像、JS、CSSといったものを置いたりする。
https://qiita.com/shosho/items/93cbff79376c41c3a30b (参考サイト)
このpublicはブラウザからサーバーにアクセスした時には公開ディレクトリとなっていることが特徴(セキュリティ的に弱いディレクトリである)
つまりは公開ディレクトリなので、あまり情報はおきたくないところです。

3.2.2.下のpublicディレクトリ

まずstorageディレクトリについて
Bladeテンプレートをコンパイルしたものやセッションのファイル、キャッシュファイル、その他フレームワークが作り出したファイルなんかが置かれる。

storage以下の階層
ディレクトリは app framework logs の3つに分かれている。
このうちの「app」のなかに「public」は存在する。

storage/app/publicについて
storage/app/public についてはユーザーのプロフィールアバターといったいろんなとこで使いたくなるようなファイルを置く場所で、非公開ディレクトリ。
https://laracasts.com/series/whats-new-in-laravel-5-3/episodes/12 (参考サイト)

Laravel 5.3からアップロードファイルの格納が簡単にできるようになり、その置き場所となっているそう。
つまりは、あまり外部に漏らしたくないアップロードされた画像を格納しておくところぐらいの解釈で良いかなと思ってます。

3.2.3.publicまとめ

上のpublic = 公開ディレクトリ
下のpublic = 非公開ディレクトリ
いったんこれだけ抑えておけば事足りるかと思います

結論をいうと、
フォームから送信された画像データは「下のpublic」(非公開ディレクトリ)に格納します。
この二つの「public」のせいでかなり混乱しましたが、それぞれ存在意義があります。
というのも、僕らがブラウザでショッピングサイトを開いても、公開されているのは上のpublicであって、画像データがあるのは下のpublicです。
ではどうやって下のpublicから画像を引っ張ってきているのか..?
答えは次項のシンボリックリンクで

3.3.シンボリックリンクについて

最初この言葉を見た時、意味不明だったので簡単に記載します。

3.3.1シンボリックリンクとは??

ショートカットのようなものです。今回の例で言うと、laravelアプリの、上のpublicが、ウェブ上に公開されるディレクトリとなります。ブラウザからサーバー上のファイルにアクセスするときは、画像は /storage/app/public(下のpublic)の中にあるため、表示することができません。そこで、上のpublic/storage(後に、上のpublicのなかにstorageを作成します) と、下のstorage/app/public にリンクを持たせることにより、public/storageにアクセスする=storage/app/publicにアクセスする、ということを実現できます。(間違っていたらすみません)
https://qiita.com/si-ma/items/16565d925b0558cbba58 (参考サイト)

つまりは、シンボリックリンクとやらを行うと、公開されてる上のpublicから非公開である下のpublicにアクセスできるようになるということ(多分)
感覚的には上のpublicと下のpublicは全く別物ではなく「表と裏の存在」のようなイメージ。

3.3.2シンボリックリンクを張る

というわけでシンボリックリンクを張っていきます。

ターミナル上にて(artisanファイルがあるディレクトリで)

php artisan storage:link

を実行すると...
スクリーンショット 2021-02-20 17.37.12.png

上のpublicのなかにstorageファイルができました。
そしてその右側に「↩️」こんなマークができました。(リンクが張られているマークだと思います)
これで、下のpublicにアクセスが可能となったはずです。

3.4.画像データの保存先を理解する

ここまでで何度か記載しましたが、重要なので大項目で記載しておきます。
画像データの保存先は下のpublicとなります

3.5.DBへの保存について

3.5.1.保存する内容

前項でも書きましたがDBに保存するのは
下のpublicに保存した画像データのファイルパス
となります。

「下のpublic」 と 「DB」 に保存するデータ内容抑えておくと考えがまとまりやすいです。

※DBに画像データを保存するやり方もあります。ですが推奨はファイルパスのみをDBに保存する方法だそうです。
ーー番外編ーー
アップロード画像をDBに保存しないほうがいい理由
・レコードのデータ量が多くなり、クエリに時間がかかる
・WebとDBを分割しようと思った時に弊害がある
・DBのストレージ容量を圧迫する
・ネットワークを圧迫する
・メンテナンス性が低下する
・キャッシュしにくくなる
どこかのサイトで見ましたが、URLがわかりませんでした。。
ーーーーーーー

3.5.2.DBに保存するために

僕の場合画像アップロード機能をつけることは当初の設計に入っていなかったので、migrationファイルからいじる必要がありました。
つまりは、新しくファイルパスをいれるカラムが必要となったので作成しておかないといけません。
既にmigrationファイルを作成してしまっている人でも、すぐに追加することができます。

画像のファイルパスを入れたいmigrationファイルに以下のコードを追加してください

$table->string('product_image');  //カラム名は好きなものでOK

完成した僕のmigrationファイルです↓↓

//略
    public function up()
    {
        Schema::create('m_products', function (Blueprint $table) {
            $table->increments('id');
            $table->string('product_name', 64);
            $table->integer('category_id')->unsigned();
            $table->integer('price')->unsigned();
            $table->integer('sale_status_id')->unsigned();
            $table->integer('product_status_id')->unsigned();
            $table->string('description', 256);
            $table->string('product_image');          //←ここに入れました
            $table->timestamp('resist_date');
            $table->integer('user_id')->unsigned();
            $table->char('delete_flag', 1);
            $table->foreign('sale_status_id')->references('id')->on('m_sales_statuses')->onDelete('cascade');
            $table->foreign('product_status_id')->references('id')->on('m_products_statuses')->onDelete('cascade');
            $table->timestamps();
        });
    }
//略

これで、DBでのファイルパスの受け皿も完成しました。
では実装していきます。

4.実装

4.1.Form部分

{!! Form::open(['route' => 'back_product_store', 'enctype'=>'multipart/form-data']) !!}

//略

<div class="form-group-sm">
    {!! Form::label('image', '商品画像', ['class' => 'd-block mt-2 mb-0']) !!}
    <input type="file" name="product_image" value="" class="ml-3 mr-2 d-inline">
</div>

//略

{!! Form::close() !!}

※bootstrapとLaravelCollectiveを用いているので、少し書き方が独特な部分があります。

ポイント
①'enctype'=>'multipart/form-data'
ファイルのアップロードを行う場合は、enctype=”multipart/form-data”は忘れずにform要素に設定をしてください。

②type="file"
取り扱うデータはfileなのでtype属性はfileに設定します

③name="product_image"
送信するデータの名前です。なんでもいいです。
僕の場合は商品画像なのでproduct_imageとしました。

④'route' => 'back_product_store'
このフォームを送信するとback_product_storeという名前のルーティングにいきます。

4.2.Routing

今回の場合は、、

Route::post('product/store', 'BackProductController@store')->name('back_product_store');

ルーティング部分に関しては特に注意事項はないです。

このルーティングで
BackProductController の storeアクションにいきます。

4.3.Controller

    public function store(CreateProductRequest $request)
    {
        //バリデーションの記載
        $this->validate($request, CreateProductRequest::rules());
        $productImage = $request->product_image;
        if ($productImage) {

            //一意のファイル名を自動生成しつつ保存し、かつファイルパス($productImagePath)を生成
            //ここでstore()メソッドを使っているが、これは画像データをstorageに保存している
            $productImagePath = $productImage->store('public/uploads');
        } else {
            $productImagePath = "";
        }

        $user = Auth::user();
        if ($user->id) {
            $userId = $user->id;
        }
        //userIdとproductImageが存在すれば以下の項目をMProductテーブルに保存
        if ($userId && $productImage) {
            $data = [
                'product_name'      => $request->productName,
                'category_id'       => $request->categoryId,
                'price'             => $request->price,
                'sale_status_id'    => $request->saleStatusId,
                'product_status_id' => $request->productStatusId,
                'description'       => $request->description,
                'user_id'           => $userId,
                'resist_date'       => date('Y-m-d H:i:s'),
                //DBにはファイルパスを保存!!!!!!
                'product_image'     => $productImagePath,
                'delete_flag'       => '',
            ];
           //$dataをクリエイトする
           MProduct::create($data);
        }
    }

ポイント
①バリデーションについて

$this->validate($request, CreateProductRequest::rules());

今回はクラスを使ってるので少しややこしい書き方です、、
調べれば、これとは別に基本的な書き方がすぐ出てくると思うので割愛させていただきます。

ファイルパスの作成及び画像データの保存を1行で記載 ※超重要※

$productImagePath = $productImage->store('public/uploads');

この一文、超重要です。
全てがここに集約されていると言っても過言ではないくらい重要です。

解説)
・前提として、$productImageは、送信されてきた画像データです。
・まず$productImagePathという変数を作ります。
・右側にて、$productImageをstore()関数で保存します。
 保存場所は引数にある'public/uploads'です。
 つまりpublicディレクトリのなかのuploadsに保存します。
・「uploadsディレクトリ」は準備していませんでしたが、保存の際に自動的に作成されます。
・ここでのpublicはもちろん「下のpublicディレクトリ」です。
・この時保存されるファイル名はstore()関数によって乱数的に作成されます。
 (これによりプロフィール画像など、ファイル名が被って欲しくない時には有効です。ファイル名を固定したければstoreAs()関数を使います。)

そして
・作った変数$productImagePathの値として何が入るかというと、
 この変数名からお察しかと思いますが
 "保存された場所へのパス"がこの変数の値となります。(ここで感動しました)

コードのコメントアウトにあるように、このコード1行のみで
//一意のファイル名を自動生成しつつ保存し、かつファイルパス($productImagePath)を生成
//ここでstore()メソッドを使っているが、これは画像データをstorageに保存している
これだけのことを行っています。

③DBにファイルパスを保存

//DBにはファイルパスを保存!!!!!!
'product_image'     => $productImagePath,

少しカラム数が多くてわかりにくいですが、やってることで重要なのはこの部分です。
product_imageというカラムに $productImagePath (ファイルパス)を保存します
dd()で取り出してみるとわかりやすいので見てみます。

以下のコメントアウト部分でデバッグをかけてみます

BackProductController
    public function store(CreateProductRequest $request)
    {
        //バリデーションの記載
        $this->validate($request, CreateProductRequest::rules());
        $productImage = $request->product_image;
        if ($productImage) {

            //一意のファイル名を自動生成しつつ保存し、かつファイルパス($productImagePath)を生成
            //ここでstore()メソッドを使っているが、これは画像データをstorageに保存している
            $productImagePath = $productImage->store('public/uploads');

            dd($productImagePath);    //ここでデバッグをかけてみる

        } else {
            $productImagePath = "";
        }

結果がこちら

スクリーンショット 2021-02-20 22.30.10.png

このようなデータが取れました。
これはファイルパスなので画像データの位置情報です。

public/uploads/にある7qaJNxnb4cKml1aOwUWoOVUoHdaAUos1U55SkYU2.jpgという画像

という意味です(そのままですみません)

というわけでしっかりとファイルパスも設定できているのでこのままDBに保存してしまえば、この後viewで表示したい時にそのパスを頼りに、下のpublic(の中のuploads)から画像データを引っ張り出せるということになります。

4.4.View(画像を表示)

最後にサーバー(Laravel)に保存した画像データを、データベースに保存したファイルパスを使ってviewに表示させます。

表示に関しては冒頭で「少しややこしい」と言いましたが、すぐ理解できるはずです。

結論から書くとこうです。

<img class="product_image" src="{{ Storage::url($product->product_image) }}" alt="" width="150px" height="100px">

class=""とか、alt=""に関してはつけたい方は適当につけてください。
width=""やheight=""は割愛します。

大事な部分はここです。

src="{{ Storage::url($product->product_image) }}"

分けて考えます。
まずは
Storage::url() の部分ですが、
指定したファイルのURLを取得するには、urlメソッドを使います。
Storage::をつけてurl()メソッドを使用することで、その引数のファイルパスを取得することができます。(言い回しがあってるかわかりませんが)

詳しくはリファレンスに記載があります。
https://readouble.com/laravel/5.5/ja/filesystem.html

次に
$product->product_image の部分ですが

$productは僕が設定した商品データの変数です。
なので
$product->product_image と記載することで
その商品のファイルパスを表しています。

つまり、最初のコードの src は「ある商品のファイルパス」を取得していたことになるので、その商品にあった場所に適した画像を表示できるわけです。

というわけで、実際のView画像である

スクリーンショット 2021-02-20 15.31.22.png

こちらにたどり着きます。

画像アップロードの解説は以上となります。

5.まとめ

何がどこに保存されているかわからなくて、大いに躓いたので記事にしました。
データの動きをみることがすごく大切だと痛感しました。。

僕なりの解釈でこの記事を書きましたが、不備などありましたらご指摘頂けると幸いです。
以下参考にさせていただいたサイトです。

参考サイト一覧
https://qiita.com/ryo-program/items/35bbe8fc3c5da1993366
https://note.com/akina7/n/ne9af79fea62e
https://reffect.co.jp/laravel/how_to_upload_file_in_laravel#i-13
https://qiita.com/koru1893/items/1d2f522e20744b03e3ad#%E5%BE%8C%E3%81%AF%E7%A7%BB%E5%8B%95%E3%81%95%E3%81%9B%E3%81%A6db%E3%81%B8%E4%BF%9D%E5%AD%98
https://laraweb.net/tutorial/2707/
https://note.com/laravelstudy/n/n038bd68f53a7#nRJwi
https://qiita.com/u-dai/items/8a904cc7fd2795c0e70d
https://biz.addisteria.com/image-upload/
https://qiita.com/shosho/items/93cbff79376c41c3a30b
https://qiita.com/si-ma/items/16565d925b0558cbba58
https://reffect.co.jp/laravel/laravel-storage-manipulation-master#visibility

最後まで読んでいただき、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPで指定日付前を取得する

はじめに

調べる機会がありましたので、備考録としてまとめました。
日付取得で使用する関数はdate関数strtotime関数である。

取得方法

date("Ymd", strtotime("-1 day")); //現在日より1日前を取得
date("Ymd", strtotime("-1 week")); //現在日より1週間前を取得
date("Ymd", strtotime("-1 month")); //現在日より1ヶ月前を取得
date("Ymd", strtotime("-1 year")); //現在日より1年前を取得
date("Ymd", strtotime('last Saturday')); //現在日より先週の日曜日の日付を取得
date("Ymd", strtotime("-1 day" ,strtotime("20210211"))); //2021年2月11日より1日前を取得

※指定日付後を取得したい場合は、上記サンプルコードのマイナスを消す。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelでFullcalendarに登録したイベントを削除する方法

はじめに

こちらは以下の記事の続きになります。
LaravelでFullcalendarを実装する方
LaravelでFullcalendarに登録した内容を更新する方法
ディレクトリ名等も同じものを使用しているので、ご了承くださいませ。

前回までにFullcalendarにイベントの登録と更新はできるようになったので、今回は削除方法について見ていきたいと思います。

-各バージョン
-Laravel 6.x
-PHP 7.4.9
-MySQL 5.7.30
-Fullcalendar v5

削除ボタンを作成する

前回までに更新用のモーダルを開くと、登録されている情報が確認できているようにしました。
今回は更新用のモーダルに削除ボタンを追加し、このボタンを押すとデータが削除されるようにします。

modal.blade.php
<div class="modal micromodal-slide" id="modal-1" aria-hidden="true">
    <div class="modal__overlay" tabindex="-1" data-micromodal-close>
        <div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
            <header class="modal__header">
                <h2>Editing my task list</h2>
                <button class="modal__close" aria-label="Close modal" data-micromodal-close></button>
            </header>
            <main>
         <form method="POST" action="{{ route('editEvent') }}">
                 @csrf
                 <input type="hidden" id="id" value="" name="id">
           <input type="text" id="edit_title" name="title" value="">
           <input type="date" id="edit_start" name="start" value="">
           <input type="color" id="edit_color" name="textColor" value="">
                  <button class="modal__btn modal__btn-primary" type="submit">変更する</button>
          </form>
        // ここから追加
              <form id="delete-event-btn" method="POST" action="{{ route('deleteEvent') }}">
                    @csrf
                    <input type="hidden" name="id">
                    <a href="#" id="delete-event" class="cancel-btn">削除する</a>
                </form> 
        // ここまで
            </main>
        </div>
    </div>
</div>

前回と同様、カレンダーのイベントを表示させるアクションであるindexアクションではAjaxで通信時のみidを渡すようになっているので(LaravelでFullcalendarを実装する方法)、idはJSで取得するようにします。

更新時にvalueの値を取得するコードを書いているので、こちらに追加していきます。

event.blade.php
<script>
  $(document).ready(function () {
    $('#calendar').fullCalendar({
      // はじめりの曜日を月曜日に変更 デフォルトは日曜日になっており、日=0,月=1になる
      firstDay: 1,
      headerToolbar: {
                     right: 'prev,next'
                     },
      events: '/index',

    eventClick: function(info){
        document.getElementById("id").value = info.id;
        document.getElementById("edit_title").value = info.title
        document.getElementById("edit_start").value = info.start._i
        document.getElementById("edit_color").value = info.textColor
        MicroModal.show('modal-1');
     }
    });

    // ここから追加
    $("#delete-event").on("click", function() {
     var form = document.getElementById("delete-event-btn");
     var eventId = document.getElementById("id").value; // 削除対象のidを前回作成した更新フォームから取得
     form.elements['id'].value = eventId; // 削除対象のidを削除フォームにもセット
     form.submit(); // 削除フォームを送信させる
     });
  });
</script>

コントローラー、ビューを作成する

先ほど削除ボタンのフォームで指定したactionを指定していきます。

web.php
Route::post('/deleteEvent', 'EventController@deleteEvent')->name('deleteEvent');

最後にコントローラーです。

EventController
public function deleteTask(Request $request) {
  // 送信されてきたidをEventテーブルに登録されているデータと紐付ける
  $task = DoneTask::find($request->input('id'));
  $task->delete();

  return redirect('/event');
}

実際に削除ボタンを押してみると、カレンダーとDBどちらからもデータが削除されてるかと思います。

これで完成です!

さいごに

今回でFullcalendarにデータの登録、更新、削除まで行うことができました!

Fullcalendarはまだまだアレンジがたくさんできるので、これからも公式マニュアルを見ながらアップデートしていきたいと思います。

最後まで読んでいただいてありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPでエラーの内容をブラウザに表示させる方法

背景

開発するときにエラーの内容がブラウザに表示されると開発しやすいので共有します。

サマリ

エラー内容の表示非表示の設定はphp.iniで管理されており、「display_errors=On」に設定することでエラー内容の表示が可能になります。

設定方法(概要)

1、php.iniのdisplay_errorsをOnに設定
2、apacheを再起動

設定方法(詳細)

0、設定前の状況を確認

index.php
<?php
//文末の「セミコロン(;)」をあえてつけずエラーにする
echo "This is study.localhost page"

index.phpにアクセス↓(http erroe 500は。phpエラーなどが原因のwebサーバーに起因するエラーなので、想定するエラーが出されています。)
image.png

1、php.iniのdisplay_errorsをOnに設定
①php.iniの場所を探す
 find関数でファイルの場所を探します。

[xxx@localhost /]# find / -name php.ini
/etc/php.ini

②php.iniを開く

[xxx@localhost /]# vi /etc/php.ini

③vi内で/display_errosを入力し該当箇所を検索
※viの検索の仕方はググってみてね

/etc/php.ini
;   On or stdout = Display errors to STDOUT
; Default Value: On
; Development Value: On
; Production Value: Off
; http://php.net/display-errors
display_errors = On

④保存後、apacheを再起動
※viの上書き保存方法は「/wq」
※設定ファイルのバックアップはご自由に

[xxx@localhost /]# systemctl restart httpd

⑤エラーが表示されることを確認
Parse error: syntax error, unexpected end of file, expecting ',' or ';' in /var/www/html/study.localhost/index.php on line 3
image.png

3行目にエラーがあると記述があり、正しいエラーが表示されているとわかります。

index.php
<?php
//文末の「セミコロン(;)」をあえてつけずエラーにする
echo "This is study.localhost page" //←3行目のセミコロンが無いためエラーになる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FormRequestクラスをテストする

動機

PHP/Laravelを触り始めて間もないが、HTTPリクエストのバリデーションでFormRequestが便利ということで使っていた。手軽だったのでHTTPテストでテストを書いていたが、非効率な箇所もあったのでFormRequest単体でのテストを書きたかった。

前提

  • PHPUnit 9.5.2
  • Laravel Framework 8.27.0

内容

テスト対象のクラスを定義する

$ php artisan make:request ExampleRequest

authorize() で真を返すようにし、 rules() で適当なバリデーションルールを追加する。

app/Http/Requests/ExampleRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ExampleRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'id' => 'required',
            'name' => 'required',
            'email' => 'required'
        ];
    }
}

テストを書く

テストファイルを作成する。(UnitかFeatureかの議論はここでは取り扱わない)

$ php artisan make:test ExampleTest

方法は以下の通り。 Table Driven Test 風に書きたかったので、データプロバイダを定義している。

  • ExampleTestインスタンスを作成する
  • Validatorインスタンスを作成する。テストデータをテスト関数の引数から、ルールとメッセージはExampleTestインスタンスから渡す
  • バリデータを実行し、その結果を評価する
tests/Feature/ExampleTest.php
<?php

namespace Tests\Feature;

use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
use App\Http\Requests\ExampleRequest;

class ExampleTest extends TestCase
{
    /**
     * @dataProvider validationProvider
     * @return void
     */
    public function testValidation($inData, $outFail, $outMessage)
    {
        $request = new ExampleRequest();
        $rules = $request->rules();
        $messages = $request->messages();
        $validator = Validator::make($inData, $rules, $messages);
        $result = $validator->fails();
        $this->assertEquals($outFail, $result);
        $messages = $validator->errors()->getMessages();
        $this->assertEquals($outMessage, $messages);
    }

    public function validationProvider()
    {
        return [
            'success' => [
                [
                    'id' => 1,
                    'name' => 'aaa',
                    'email' => 'aaaa@example.com',
                ],
                false,
                [],
            ],
            'empty all fields' => [
                [],
                true,
                [
                    'id' => ['The id field is required.'],
                    'name' => ['The name field is required.'],
                    'email' => ['The email field is required.'],
                ],
            ],
        ];
    }
}

注意点

HTTPテストをやめて書く場合、当然ながらそのテストで確認していた点はテストできないので注意(例えば、FormRequestが返すメッセージ構造とHTTPレスポンスの構造は異なるなど。)

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[PHP][初心者] LINEBOTでデバッグしたいならerror_log関数を使ってみて

本記事の趣旨

こんにちは!今回が二回目の投稿になります。一回目に引き続き、本記事も筆者がLINEBOTを始めて作成した時に、困ったこと(最初に知っておきたかった)について書いています。

これまでPHP、CakePHP、Laravelを触ってきて、「デバッグってめっちゃ便利やん!最高!」って思ってきました。ところがLINEBOTを作っている時、「あれ?ユーザーが送信した値ってどう確認するんだ?」と大詰まり。というのもユーザーが送信した値って、当たり前ですがブラウザには反映されないんですよね。LINEBOT初心者だったので、どうじてもブラウザで値の確認がしたいと思って探しているうちにこの記事に出会いました。

[PHP]LINE bot開発での簡易デバッグ

それではLINEBOTでデバッグする方法を解説していきましょう!

目次

1.結論
2.error_log関数を使ってみる
 -使い方
 -やってみよう!
3.LINEBOTでデバッグする
 -デバッグする際の注意点
 -いろいろなメッセージを送信して$eventの中身を確認しよう!
4.まとめ

1.結論

結論を言うと、LINEBOTでデバッグするにはerror_log関数を使う必要があります。(他にも方法があるかもしれません)
まずは、error_logの基本的な使い方から見ていきましょう!

2.error_log関数を使ってみる

error_log関数は、ログの出力をする関数です。プログラムを実行した際にログ(記録)をファイルに書き込むことでエラー内容を確認することができます。

PHPのerror_logメソッドでログを出力する方法を現役エンジニアが解説【初心者向け】
上記の記事を参考に解説していきます。

-使い方

PHP公式より

error_log ( string $message , int $message_type = 0 , string $destination = ? , string $extra_headers = ? ) : bool

1.message:出力するメッセージ。
2.message_type:メッセージタイプ。0~4までのオプションがある。今回はファイルに書き込みたいので3を使用する。
3.destination:出力先。今回はファイルに書き込みたいので出力先はファイル名となる。
4.extra_headers:メッセージタイプが1(メールへ送信)の時に使われる。今回は使用しない。

-やってみよう!

まずはテストファイルを作ります。
test.phpとdebug.logを作ってください。

test.php
<?php
if (error_log('ログテスト' . "\n", 3, 'debug.log')) {
    echo 'ログ出力成功';
} else {
    echo 'ログ出力失敗';
}

メッセージは改行されずに出力されるので改行コード(\n)を書いています。また、ファイルの拡張子は.logを使います。
このままやるとエラーが出ると思います。

結果
test.php(ブラウザ)

Warning: error_log(debug.log): failed to open stream: Permission denied in /xxx/test.php on line 2
ログ出力失敗

要は書き込みする権限がないということなので、権限を与えてあげましょう。

$ chmod 777 debug.log

結果
test.php(ブラウザ)

ログ出力成功

debug.log(ブラウザ)

ログテスト

これでログを出力することができました!次はLINEBOTで同じようにやってみましょう。

3.LINEBOTでデバッグする

LINEBOTをまだ作っていない方は以下の記事を参考にして作ってみてください(自分のやつ笑)。

【PHP】LINE Messaging APIのオウム返しBOTを簡単に解説する

-デバッグする際の注意点

webhook.php
<?php

/**
 * Copyright 2016 LINE Corporation
 *
 * LINE Corporation licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

require_once('./LINEBotTiny.php');

$channelAccessToken = 'XXXXXX';
$channelSecret = 'XXXXXX';

$client = new LINEBotTiny($channelAccessToken, $channelSecret);
foreach ($client->parseEvents() as $event) {
    switch ($event['type']) {
        case 'message':
            $message = $event['message'];
            switch ($message['type']) {
                case 'text':
                    $client->replyMessage([
                        'replyToken' => $event['replyToken'],
                        'messages' => [
                            [
                                'type' => 'text',
                                'text' => $message['text']
                            ]
                        ]
                    ]);
                    break;
                default:
                    error_log('Unsupported message type: ' . $message['type']);
                    break;
            }
            break;
        default:
            error_log('Unsupported event type: ' . $event['type']);
            break;
    }
    error_log(print_r($event, true) . "\n", 3, 'debug.log'); //ここに追記
};

最後から2番目の行にコードを追加しました。

webhook.php
error_log(print_r($event, true) . "\n", 3, 'debug.log'); //ここに追記

(注)先ほどは第一引数にそのまま文字列を書いていましたが、今回は配列の中身を見たいのでprint_rを使います。trueとすることでstringを返します。PHP公式参照

-いろいろなメッセージを送信して$eventの中身を確認しよう!

↓「おはよう」と送信して結果

debug.log
Array
(
    [type] => message
    [replyToken] => XXXXXX
    [source] => Array
        (
            [userId] => XXXXXX
            [type] => user
        )
    [timestamp] => 1613776839012
    [mode] => active
    [message] => Array
        (
            [type] => text
            [id] => XXXXXX
            [text] => おはよう
        )

)

↓位置情報を送った結果

debug.log
Array
(
    [type] => message
    [replyToken] =>XXXXXX
    [source] => Array
        (
            [userId] => XXXXXX
            [type] => user
        )

    [timestamp] => 1613777326632
    [mode] => active
    [message] => Array
        (
            [type] => location
            [id] => XXXXXX
            [address] => 日本、〒516-0024 三重県伊勢市宇治今在家町
            [latitude] => 34.420623
            [longitude] => 136.716155
        )

)

↓スタンプを送った結果

debug.log
Array
(
    [type] => message
    [replyToken] => XXXXXX
    [source] => Array
        (
            [userId] => XXXXXX
            [type] => user
        )

    [timestamp] => 1613777361721
    [mode] => active
    [message] => Array
        (
            [type] => sticker
            [id] => XXXXXX
            [stickerId] => 52002740
            [packageId] => 11537
            [stickerResourceType] => ANIMATION
            [keywords] => Array
                (
                    [0] => roger
                    [1] => np
                    [2] => understood
                    [3] => ofcourse
                    [4] => line
                    [5] => Affirmative
                    [6] => okie
                    [7] => cony
                    [8] => understand
                    [9] => noworries
                    [10] => OK
                    [11] => isee
                )

        )

)

4.まとめ

①LINEBOTでデバッグしたい時はerror_log関数を使う
②配列の中身を見たいので、error_logのメッセージ(第一引数)はprint_rで出力するようにする

もちろん公式ドキュメントを見れば済む話なのですが、個人的には実際に値を送って、その値をデバッグした方が理解が深まるように感じます。また、値を確認するだけでなく、本来の使い方であるバグ(どこでつまっているのか)を見つけたい際にも活用することができます。「あれ、返信が返ってこないな?」という時にはerror_log関数を使ってデバッグしましょう!

最後まで読んでいただき、ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】Laravel6で同時ログイン制御(先勝ち)をする方法を考えてみた

この記事について

この間に投稿した記事でLaravelでは認証機能(ログイン機能)を設定するのは簡単だと説明しました。
【PHP】Laravel6で遊ぶ(認証機能のセットアップ)

認証機能で考えてみたいのが、『同時ログイン制御』です。
少し調べたところ、あまり同時ログイン制御に関する記事やサイトがないような感じです。
本記事では先勝ちで同時ログインを禁止する方法を検討してみます。

環境

OS:Windows10 Home
PHP:7.4.15(XAMPP)
Laravel:6.20.16

実現方法

昨日の記事でSessionの管理方法について触れてみました。
【PHP】Laravelのセッション管理を勉強する

この記事では、Session情報をDBで管理する方法を紹介し、Sessionsテーブルに以下の項目が管理できています。

カラム名 内容
id セッションID
user_id ログインしているユーザーのID(NULLの場合は未ログイン)
ip_address IPアドレス
user_agent ユーザーエージェント
payload いろいろなデータ(適当)
last_activity 最終行動時間(UNIX時間)

実現方法としては、ログイン処理後の遷移前にログインしようとしているユーザーのIDがSessionsテーブルに存在するかどうかを調べます。
存在しない場合はログインさせ、存在する場合はログイン画面に戻す処理で考えてみます。

修正するファイルについて

実現方法のイメージはできたので、どのファイルをいじればいいのかを考えてみます。
ログイン画面のHTMLを確認すると、FormのPOSTメソッドでactionが(ドメイン)/loginになっています。
その場合にどのような処理が走るかは以下のコマンドで確認できます。

command
php artisan route:list
result
+--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+
| Domain | Method   | URI                    | Name             | Action                                                                 | Middleware   |
+--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+
|        | GET|HEAD | /                      |                  | Closure                                                                | web          |
|        | GET|HEAD | api/user               |                  | Closure                                                                | api,auth:api |
|        | GET|HEAD | home                   | home             | App\Http\Controllers\HomeController@index                              | web,auth     |
|        | GET|HEAD | login                  | login            | App\Http\Controllers\Auth\LoginController@showLoginForm                | web,guest    |
|        | POST     | login                  |                  | App\Http\Controllers\Auth\LoginController@login                        | web,guest    |
|        | POST     | logout                 | logout           | App\Http\Controllers\Auth\LoginController@logout                       | web          |
|        | GET|HEAD | password/confirm       | password.confirm | App\Http\Controllers\Auth\ConfirmPasswordController@showConfirmForm    | web,auth     |
|        | POST     | password/confirm       |                  | App\Http\Controllers\Auth\ConfirmPasswordController@confirm            | web,auth     |
|        | POST     | password/email         | password.email   | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail  | web          |
|        | GET|HEAD | password/reset         | password.request | App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm | web          |
|        | POST     | password/reset         | password.update  | App\Http\Controllers\Auth\ResetPasswordController@reset                | web          |
|        | GET|HEAD | password/reset/{token} | password.reset   | App\Http\Controllers\Auth\ResetPasswordController@showResetForm        | web          |
|        | GET|HEAD | register               | register         | App\Http\Controllers\Auth\RegisterController@showRegistrationForm      | web,guest    |
|        | POST     | register               |                  | App\Http\Controllers\Auth\RegisterController@register                  | web,guest    |
+--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+

上記のようなRouteの情報はroute/web.phpファイルで確認することもできます。

route/web.php
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

ただ、認証機能をセットアップするとAuth::routes();で認証関係のRouteがまとめられているので、コマンドで調べました。

コマンドの結果に戻ります。
ログイン処理時には/loginにPOSTメソッドで遷移します。
コマンド結果を見てみると・・・

+--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+
| Domain | Method   | URI                    | Name             | Action                                                                 | Middleware   |
+--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+
|        | POST     | login                  |                  | App\Http\Controllers\Auth\LoginController@login  

これですね。
アクションとしては、LoginControllerloginメソッドに処理を渡していることが分かります。
なので、App\Http\Controllers\Auth\LoginController.phpを修正すればよさそうですね。

loginメソッドの確認

早速、LoginController.phpを確認してみます。

LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }
}

あれ?loginメソッドは?ってなりました。
実際は上記ファイルにloginメソッドの詳細は記載されていません。
AuthenticatesUsersというのがポイントで、Illuminate/Foundation/Auth/AuthenticatesUsersに記載されています。
ちなみに、Illuminate/vendor/laravel/framework/src/Illuminateを指しています。

/vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticatesUsers.php
<?php

namespace Illuminate\Foundation\Auth;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

trait AuthenticatesUsers
{
    use RedirectsUsers, ThrottlesLogins;

    /**
     * Show the application's login form.
     *
     * @return \Illuminate\Http\Response
     */
    public function showLoginForm()
    {
        return view('auth.login');
    }

    /**
     * Handle a login request to the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function login(Request $request)
    {
        $this->validateLogin($request);

        // If the class is using the ThrottlesLogins trait, we can automatically throttle
        // the login attempts for this application. We'll key this by the username and
        // the IP address of the client making these requests into this application.
        if (method_exists($this, 'hasTooManyLoginAttempts') &&
            $this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

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

        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }

        // If the login attempt was unsuccessful we will increment the number of attempts
        // to login and redirect the user back to the login form. Of course, when this
        // user surpasses their maximum number of attempts they will get locked out.
        $this->incrementLoginAttempts($request);

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

    /**
     * Validate the user login request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    protected function validateLogin(Request $request)
    {
        $request->validate([
            $this->username() => 'required|string',
            'password' => 'required|string',
        ]);
    }

    /**
     * Attempt to log the user into the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    protected function attemptLogin(Request $request)
    {
        return $this->guard()->attempt(
            $this->credentials($request), $request->filled('remember')
        );
    }

    /**
     * Get the needed authorization credentials from the request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    protected function credentials(Request $request)
    {
        return $request->only($this->username(), 'password');
    }

    /**
     * Send the response after the user was authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendLoginResponse(Request $request)
    {
        $request->session()->regenerate();

        $this->clearLoginAttempts($request);

        return $this->authenticated($request, $this->guard()->user())
                ?: redirect()->intended($this->redirectPath());
    }

    /**
     * The user has been authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  mixed  $user
     * @return mixed
     */
    protected function authenticated(Request $request, $user)
    {
        //
    }

    /**
     * Get the failed login response instance.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Symfony\Component\HttpFoundation\Response
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    protected function sendFailedLoginResponse(Request $request)
    {
        throw ValidationException::withMessages([
            $this->username() => [trans('auth.failed')],
        ]);
    }

    /**
     * Get the login username to be used by the controller.
     *
     * @return string
     */
    public function username()
    {
        return 'email';
    }

    /**
     * Log the user out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function logout(Request $request)
    {
        $this->guard()->logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return $this->loggedOut($request) ?: redirect('/');
    }

    /**
     * The user has logged out of the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    protected function loggedOut(Request $request)
    {
        //
    }

    /**
     * Get the guard to be used during authentication.
     *
     * @return \Illuminate\Contracts\Auth\StatefulGuard
     */
    protected function guard()
    {
        return Auth::guard();
    }
}

長いですが、loginメソッドが記載されていましたね。
内容までは解説しませんが・・・

ただ、loginメソッドが上記ファイルに記載されているとは言っても、上記ファイルを編集することはおススメしません。
vendorフォルダにはComposerでインストールしたライブラリ等が含まれるので、影響範囲が大きいです。
処理の制御はControllerで行うのが基本になりますので、LoginControllerの方を修正することにします。

LoginControllerの修正

以下のように修正してみました。

LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request; //追加
use Illuminate\Support\Facades\Auth; //追加
use Illuminate\Support\Facades\DB; //追加


class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    //以下を修正
    use AuthenticatesUsers{
        login as _login;
    }

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    //以下を追記
    public function login(Request $request){

        $response = $this->_login($request);

        $user_id = Auth::id();

        $count = DB::table("sessions")
        ->where("user_id", $user_id)
        ->count();

        if ($count === 0){
            return $response;
        }
        else{
            Auth::logout();
            \Session::flash("message", "他のユーザーがログインしています。時間を置いて再ログインしてください。");
            return view("auth.login");
        }
    }
}

解説

    use AuthenticatesUsers{
        login as _login;
    }

これによって、後述する自作のloginメソッドに向くようにしています。

    public function login(Request $request){

        $response = $this->_login($request);

        $user_id = Auth::id();

        $count = DB::table("sessions")
        ->where("user_id", $user_id)
        ->count();

        if ($count === 0){
            return $response;
        }
        else{
            Auth::logout();
            \Session::flash("message", "他のユーザーがログインしています。時間を置いて再ログインしてください。");
            return view("auth.login");
        }
    }

こちらが追記したloginメソッドです。
リネームした_loginメソッドを実行してreturnの前に分岐を入れています。

ここでは詳しい説明はしませんが、Authファザードを使ってログインしたユーザーのIDを取得して、DBファザードを使ってSQLの実行をしています。

既にログインされている場合は、自信をログアウトさせてログイン画面に遷移するようにしています。

\Session::flash("message", "他のユーザーがログインしています。時間を置いて再ログインしてください。");

ちなみに、フラッシュと言って、遷移先に1回だけメッセージを表示することができます。

ということで、メッセージを表示するlogin.blade.phpにメッセージ表示するよう追記します。

/resources/views/auth/login.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <!--追記-->
            @if (session('message'))
                <div class="alert alert-warning">
                    {{ session('message') }}
                </div>
            @endif
            <!--ここまで-->
            <div class="card">
                <div class="card-header">{{ __('Login') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('login') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>

                                @error('email')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">

                                @error('password')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <div class="col-md-6 offset-md-4">
                                <div class="form-check">
                                    <input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>

                                    <label class="form-check-label" for="remember">
                                        {{ __('Remember Me') }}
                                    </label>
                                </div>
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-8 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Login') }}
                                </button>

                                @if (Route::has('password.request'))
                                    <a class="btn btn-link" href="{{ route('password.request') }}">
                                        {{ __('Forgot Your Password?') }}
                                    </a>
                                @endif
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

検証結果

Chromeでログインしてみる(1人目)
image.png
1人目としてなので、ログインできてOKです!

Firefoxでログインしてみる(2人目)
image.png
2人目なのでログインできずに、未ログインの状態でログイン画面に戻っていますね!
メッセージも表示されていていい感じです!

完成?

上記内容でうまく同時ログイン制御ができています。
ただ、一つ考慮しないといけないことがあります。
この内容でうまく処理ができるのは「ログアウト処理をちゃんとしている」場合です。

よくあると思いますが、皆さんログアウト処理をせずにブラウザのタブを閉じたり、ブラウザ自体を閉じたりして作業を終了させること多いと思います。
その場合、sessionsテーブルのuser_is``に値が入ったままで残り続けます・・・
そうなると、一生ログインできなくなってしまいます・・・

もう1つの条件

ここで使うのがlast_activityカラムです。
このカラムには最終行動したUNIX時間が格納されます。

なので、例えば20分操作しなかった場合にログアウトしたとみなすものとします。
それならば、ログインできる条件を「自アカウントでログインしている、かつ、現在時刻と最終行動時間の差が1200秒以内であるユーザーが0人」とすれば良いと考えます。

なので、以下のようにLoginControllerloginメソッドに条件を追加します。

    public function login(Request $request){

        $response = $this->_login($request);

        $user_id = Auth::id();
        $time = time(); //現在時刻のUNIX時間

        $count = DB::table("sessions")
        ->where("user_id", $user_id)
        ->where("last_activity", ">", $time-1200) //条件(AND)追加
        ->count();

        if ($count === 0){
            return $response;
        }
        else{
            Auth::logout();
            \Session::flash("message", "他のユーザーがログインしています。時間を置いて再ログインしてください。");
            return view("auth.login");
        }
    }

こんな感じでしょうか。
ちなみに「20分操作しなかった場合にログアウトしたとみなす」とする場合はLaravelの.envおよび、config/session.phpSESSION_LIFETIMEも20分にして合わせておくのがベストです。
逆にそうしないと2人ログインできている状態にもできるので、同時ログイン制御の目的は達成できないですね。

最後に

後勝ちの同時ログイン制御はlogoutOtherDevicesを使えるので比較的楽に実装できそうですが、
先勝ちの場合はどうしようかと思って考えついた方法です。
もっと楽な方法、リスクが少ない方法があれば教えて欲しいです^^

参考記事

Laravelで同時ログイン数を制御をする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】query,prepare,execute,fetchの意味

初めに

・PHP/MySQLを触り始めて1か月
こちらのアプリ開発をした際に思った疑問点を深く学んでみようと思い記事を執筆

課題

PHP側でデータベースから値を取得したり、データベースに値を保存したりする際に

$members = $db->prepare('...');
$members->execute('...');
$member = $members->fetch();

$member = $db->query('...');

を何気なく使っていましたが、各関数の意味をしっかり理解せずに使っていました。

現在の理解(Before)

現在の私の各関数の理解は以下の通りです。

query

SELECT文を使い、かつforeachで繰り返し処理をする際に使う関数

prepare

SELECT文を使い、かつ取り出す値が単体である(繰り返し処理等をしない)際に使う関数

execute

prepareとペアで使うので、prepareで取得したデータベース上の値をPHPで受け取る

fetch

executeで取り出した値を列毎に配列で受け取る

学習結果(After)

query

・SQLステートメントを実行し、結果セットをPDOStatementオブジェクトとして返す
 ⇒prepareとexecuteを一緒に実行する
・prepareとの違い:変動値がない場合

// 変動値がないコード
$pdo->query('SELECT * FROM user');

答え合わせ:×

prepare

文を実行する準備を行い、文オブジェクトを返す
⇒実行したいSQL文をセットする
・queryとの違い:変動値がある場合

// 変動値があるコード
$stmt = $pdo->prepare("SELECT * FROM user WHERE name=:name");

答え合わせ:△

execute

SQLステートメントを実行し、作用した行数を返す
⇒SQLの実行
答え合わせ:△

fetch

結果セットから次の行を取得する
⇒executeで取得した行の値を取得する
答え合わせ:〇

まとめ

・特にqueryとprepare/executeの違いが言語化しつつ学べました。
・イメージは間違えていなかったことがわかりましたが、公式ドキュメントを見ると適切な言葉選びをされている、と実感し改めて1次情報で学習を進めることの大切さを学びました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】queryとprepare/execute,fetchの意味

初めに

・PHP/MySQLを触り始めて1か月
こちらのアプリ開発をした際に思った疑問点を深く学んでみようと思い記事を執筆

課題

PHP側でデータベースから値を取得したり、データベースに値を保存したりする際に

$members = $db->prepare('...');
$members->execute('...');
$member = $members->fetch();

$member = $db->query('...');

を何気なく使っていましたが、各関数の意味をしっかり理解せずに使っていました。

現在の理解(Before)

現在の私の各関数の理解は以下の通りです。

query

SELECT文を使い、かつforeachで繰り返し処理をする際に使う関数

prepare

SELECT文を使い、かつ取り出す値が単体である(繰り返し処理等をしない)際に使う関数

execute

prepareとペアで使うので、prepareで取得したデータベース上の値をPHPで受け取る

fetch

executeで取り出した値を列毎に配列で受け取る

学習結果(After)

query

・SQLステートメントを実行し、結果セットをPDOStatementオブジェクトとして返す
 ⇒prepareとexecuteを一緒に実行する
・prepareとの違い:変動値がない場合

// 変動値がないコード
$pdo->query('SELECT * FROM user');

答え合わせ:×

prepare

文を実行する準備を行い、文オブジェクトを返す
⇒実行したいSQL文をセットする
・queryとの違い:変動値がある場合

// 変動値があるコード
$stmt = $pdo->prepare("SELECT * FROM user WHERE name=:name");

答え合わせ:△

execute

SQLステートメントを実行し、作用した行数を返す
⇒SQLの実行
答え合わせ:△

fetch

結果セットから次の行を取得する
⇒executeで取得した行の値を取得する
答え合わせ:〇

まとめ

・特にqueryとprepare/executeの違いが言語化しつつ学べました。
・イメージは間違えていなかったことがわかりましたが、公式ドキュメントを見ると適切な言葉選びをされている、と実感し改めて1次情報で学習を進めることの大切さを学びました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】queryとprepare/execute/fetchの意味

初めに

・PHP/MySQLを触り始めて1か月
こちらのアプリ開発をした際に思った疑問点を深く学んでみようと思い記事を執筆

課題

PHP側でデータベースから値を取得したり、データベースに値を保存したりする際に

$members = $db->prepare('...');
$members->execute('...');
$member = $members->fetch();

$member = $db->query('...');

を何気なく使っていましたが、各関数の意味をしっかり理解せずに使っていました。

現在の理解(Before)

現在の私の各関数の理解は以下の通りです。

query

SELECT文を使い、かつforeachで繰り返し処理をする際に使う関数

prepare

SELECT文を使い、かつ取り出す値が単体である(繰り返し処理等をしない)際に使う関数

execute

prepareとペアで使うので、prepareで取得したデータベース上の値をPHPで受け取る

fetch

executeで取り出した値を列毎に配列で受け取る

学習結果(After)

query

・SQLステートメントを実行し、結果セットをPDOStatementオブジェクトとして返す
 ⇒prepareとexecuteを一緒に実行する
・prepareとの違い:変動値がない場合

// 変動値がないコード
$pdo->query('SELECT * FROM user');

答え合わせ:×

prepare

文を実行する準備を行い、文オブジェクトを返す
⇒実行したいSQL文をセットする
・queryとの違い:変動値がある場合

// 変動値があるコード
$stmt = $pdo->prepare("SELECT * FROM user WHERE name=:name");

答え合わせ:△

execute

SQLステートメントを実行し、作用した行数を返す
⇒SQLの実行
答え合わせ:△

fetch

結果セットから次の行を取得する
⇒executeで取得した行の値を取得する
答え合わせ:〇

まとめ

・特にqueryとprepare/executeの違いが言語化しつつ学べました。
・イメージは間違えていなかったことがわかりましたが、公式ドキュメントを見ると適切な言葉選びをされている、と実感し改めて1次情報で学習を進めることの大切さを学びました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む