20190228のPHPに関する記事は16件です。

殺人事件を Exception で再現してみる feat. ポートピア連続殺人事件

これは何?

  • 昔の有名な殺人事件を、Exception の try~catch で再現してみます。
  • Exceptionの握りつぶしについて「完全に握りつぶす」「1つ以外を握りつぶす」「一切握りつぶさない」の3パターンを説明します。
  • ソースコード(portpia.php)は最後に全部掲載します。

はじまり

事件発生!
山川社長が何者かに殺害された。
刑事のヤスが事件解決の為、捜査を開始した。

山川社長、文江、ヤスを呼び出す。

new でオブジェクトを作ります。ここではクラス定義は秘密にしておきます。(最後に掲載します)

$objs = [];
$objs[] = new yamakawa();
$objs[] = new fumie();
$objs[] = new yasu();

聴取1回目:自己紹介させます。

Exceptionに重要発言を持たせています。ここでは、自己紹介するだけなので、Exceptionを完全に握りつぶします。

foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 吐かないよう握りつぶす
        ;
    }
}

実行結果

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!

聴取2回目:事情を聴いてみます。

重要発言(ようはException)を1つだけ聴いてみます。

foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 1つだけ吐かせる
        $obj->say($e->getMessage());
    }
}

実行結果

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
山川(殺害された人)   >>> 実は、昔、詐欺をやっていたという過去があります

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!
文江(山川の秘書)     >>> 実は、ヤスは私の兄です

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!
ヤス(捜査している人) >>> 実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります

聴取3回目:真実を吐かせてみる

全ての重要発言、ようは全てのExceptionを聴いてみます。ネストしたExceptionを全て展開しているだけです。

コード

foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 全部吐かせる
        do {
            $obj->say($e->getMessage());
        } while($e = $e->getPrevious());
    }
}

実行結果

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
山川(殺害された人)   >>> 実は、昔、詐欺をやっていたという過去があります
山川(殺害された人)   >>> 実は・・・後悔しています。罪滅ぼしのため文江を秘書に雇いました

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!
文江(山川の秘書)     >>> 実は、ヤスは私の兄です
文江(山川の秘書)     >>> 実は・・・兄の犯罪に加担していました

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!
ヤス(捜査している人) >>> 実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります
ヤス(捜査している人) >>> 実は・・・詐欺の黒幕は山川社長と知り、復讐のため私が殺害しました

聴取の総括:どこで吐いたのか

事件は解決したんですが、ソースコード中のどこで重要発言があったのか(つまりExceptionが発生したのか)全部出力してみます。

foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // どこで吐いたのかも含めて、全部吐かせる
        // print $e."\n"; でいいのだけど、出力が見づらいので行頭を少し加工
        print preg_replace('/^/m', '  |', $e)."\n";
    }
}

実行結果

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
  |Exception: 実は・・・後悔しています。罪滅ぼしのため文江を秘書に雇いました in /home/ubuntu/20190227/portopia.php:31
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(23): yamakawa->deepListening()
  |#1 /home/ubuntu/20190227/portopia.php(137): yamakawa->listening()
  |#2 {main}
  |
  |Next Exception: 実は、昔、詐欺をやっていたという過去があります in /home/ubuntu/20190227/portopia.php:25
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(137): yamakawa->listening()
  |#1 {main}
:
(以降長いので省略、最後に全部載せてます)

まとめ

  • 犯人はヤス
  • 重要発言、つまり Exception は聞き漏らさないようにしましょう。
    • catch(Exception $e){ ; } すると、重要発言は何も聞けない。
    • 重要発言を聞くために catch(Exception $e){ print $e->getMessage(); } としても、1つしか聞けない。
    • 重要発言を聞き漏らさない為には catch(Exception $e){ print $e; } とする。

添付:全ソースコード

portpia.php
<?php

abstract class investigation
{
    abstract function whatName();
    abstract function listening();
    function say($msg) {
        print $this->whatName() . " >>> " . $msg . "\n";
    }
}

class yamakawa extends investigation
{
    function whatName()
    {
        return '山川(殺害された人)  ';
    }

    function listening()
    {
        $this->say("社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!");
        try {
            $this->deepListening();
        } catch (Exception $e) {
            throw new Exception("実は、昔、詐欺をやっていたという過去があります", 0, $e);
        }
    }

    function deepListening()
    {
        throw new Exception("実は・・・後悔しています。罪滅ぼしのため文江を秘書に雇いました");
    }
}

class fumie extends investigation
{
    function whatName()
    {
        return '文江(山川の秘書)    ';
    }

    function listening()
    {
        $this->say("山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!");
        try {
            $this->deepListening();
        } catch (Exception $e) {
            throw new Exception("実は、ヤスは私の兄です", 0, $e);
        }
    }

    function deepListening()
    {
        throw new Exception("実は・・・兄の犯罪に加担していました");
    }
}

class yasu extends investigation
{
    function whatName()
    {
        return 'ヤス(捜査している人)';
    }

    function listening()
    {
        $this->say("山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!");
        try {
            $this->deepListening();
        } catch (Exception $e) {
            throw new Exception("実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります", 0, $e);
        }
    }

    function deepListening()
    {
        throw new Exception("実は・・・詐欺の黒幕は山川社長と知り、復讐のため私が殺害しました");
    }
}

$objs = [];
$objs[] = new yamakawa();
$objs[] = new fumie();
$objs[] = new yasu();

print "\n";
print "***\n";
print "*** 聴取1回目:自己紹介(Exceptionを完全に握りつぶす)\n";
print "***\n";
foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 吐かないよう握りつぶす
        ;
    }
}

print "\n";
print "***\n";
print "*** 聴取2回目:事情を聴く(直近のExceptionのメッセージ1つだけ吐かせる)\n";
print "***\n";
foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 1つだけ吐かせる
        $obj->say($e->getMessage());
    }
}

print "\n";
print "***\n";
print "*** 聴取3回目:真実を吐かせる(ネストしているExceptionのメッセージも含め全部吐かせる)\n";
print "***\n";
foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 全部吐かせる
        do {
            $obj->say($e->getMessage());
        } while($e = $e->getPrevious());
    }
}

print "\n";
print "***\n";
print "*** 聴取の総括:どこで吐いたのか(Exceptionのメッセージ以外の情報も全部吐かせる)\n";
print "***\n";
foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // どこで吐いたのかも含めて、全部吐かせる
        // print $e."\n"; でいいのだけど、出力が見づらいので行頭を少し加工
        print preg_replace('/^/m', '  |', $e)."\n";
    }
}

添付:全実行結果

PHP7.2で動作確認しましたが、他のバージョンでも動くと思います。

$ php --version
PHP 7.2.15-0ubuntu0.18.04.1 (cli) (built: Feb  8 2019 14:54:22) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.15-0ubuntu0.18.04.1, Copyright (c) 1999-2018, by Zend Technologies

$ php portopia.php 

***
*** 聴取1回目:自己紹介(Exceptionを完全に握りつぶす)
***

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!

***
*** 聴取2回目:事情を聴く(直近のExceptionのメッセージ1つだけ吐かせる)
***

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
山川(殺害された人)   >>> 実は、昔、詐欺をやっていたという過去があります

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!
文江(山川の秘書)     >>> 実は、ヤスは私の兄です

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!
ヤス(捜査している人) >>> 実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります

***
*** 聴取3回目:真実を吐かせる(ネストしているExceptionのメッセージも含め全部吐かせる)
***

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
山川(殺害された人)   >>> 実は、昔、詐欺をやっていたという過去があります
山川(殺害された人)   >>> 実は・・・後悔しています。罪滅ぼしのため文江を秘書に雇いました

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!
文江(山川の秘書)     >>> 実は、ヤスは私の兄です
文江(山川の秘書)     >>> 実は・・・兄の犯罪に加担していました

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!
ヤス(捜査している人) >>> 実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります
ヤス(捜査している人) >>> 実は・・・詐欺の黒幕は山川社長と知り、復讐のため私が殺害しました

***
*** 聴取の総括:どこで吐いたのか(Exceptionのメッセージ以外の情報も全部吐かせる)
***

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
  |Exception: 実は・・・後悔しています。罪滅ぼしのため文江を秘書に雇いました in /home/ubuntu/20190227/portopia.php:31
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(23): yamakawa->deepListening()
  |#1 /home/ubuntu/20190227/portopia.php(137): yamakawa->listening()
  |#2 {main}
  |
  |Next Exception: 実は、昔、詐欺をやっていたという過去があります in /home/ubuntu/20190227/portopia.php:25
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(137): yamakawa->listening()
  |#1 {main}

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!
  |Exception: 実は・・・兄の犯罪に加担していました in /home/ubuntu/20190227/portopia.php:54
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(46): fumie->deepListening()
  |#1 /home/ubuntu/20190227/portopia.php(137): fumie->listening()
  |#2 {main}
  |
  |Next Exception: 実は、ヤスは私の兄です in /home/ubuntu/20190227/portopia.php:48
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(137): fumie->listening()
  |#1 {main}

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!
  |Exception: 実は・・・詐欺の黒幕は山川社長と知り、復讐のため私が殺害しました in /home/ubuntu/20190227/portopia.php:77
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(69): yasu->deepListening()
  |#1 /home/ubuntu/20190227/portopia.php(137): yasu->listening()
  |#2 {main}
  |
  |Next Exception: 実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります in /home/ubuntu/20190227/portopia.php:71
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(137): yasu->listening()
  |#1 {main}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

殺人事件の解決までを Exception で再現してみる feat. ポートピア連続殺人事件

これは何?

  • 昔の有名な殺人事件を、Exception の try~catch で再現してみます。
  • Exceptionの握りつぶしについて「完全に握りつぶす」「1つ以外を握りつぶす」「一切握りつぶさない」の3パターンを使って、徐々に事件の解決に近づきます。
  • ソースコード(portpia.php)は最後に全部掲載します。

はじまり

事件発生!
山川社長が何者かに殺害された。
刑事のヤスが事件解決の為、捜査を開始した。

山川社長、文江、ヤスを呼び出す。

new でオブジェクトを作ります。ここではクラス定義は秘密にしておきます。(最後に掲載します)

$objs = [];
$objs[] = new yamakawa();
$objs[] = new fumie();
$objs[] = new yasu();

聴取1回目:自己紹介させます。

Exceptionに重要発言を持たせています。ここでは、自己紹介するだけなので、Exceptionを完全に握りつぶします。

foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 吐かないよう握りつぶす
        ;
    }
}

実行結果

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!

聴取2回目:事情を聴いてみます。

重要発言(ようはException)を1つだけ聴いてみます。

foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 1つだけ吐かせる
        $obj->say($e->getMessage());
    }
}

実行結果

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
山川(殺害された人)   >>> 実は、昔、詐欺をやっていたという過去があります

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!
文江(山川の秘書)     >>> 実は、ヤスは私の兄です

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!
ヤス(捜査している人) >>> 実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります

聴取3回目:真実を吐かせてみる

全ての重要発言、ようは全てのExceptionを聴いてみます。ネストしたExceptionを全て展開しているだけです。

コード

foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 全部吐かせる
        do {
            $obj->say($e->getMessage());
        } while($e = $e->getPrevious());
    }
}

実行結果

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
山川(殺害された人)   >>> 実は、昔、詐欺をやっていたという過去があります
山川(殺害された人)   >>> 実は・・・後悔しています。罪滅ぼしのため文江を秘書に雇いました

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!
文江(山川の秘書)     >>> 実は、ヤスは私の兄です
文江(山川の秘書)     >>> 実は・・・兄の犯罪に加担していました

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!
ヤス(捜査している人) >>> 実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります
ヤス(捜査している人) >>> 実は・・・詐欺の黒幕は山川社長と知り、復讐のため私が殺害しました

聴取の総括:どこで吐いたのか

事件は解決したんですが、ソースコード中のどこで重要発言があったのか(つまりExceptionが発生したのか)全部出力してみます。

foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // どこで吐いたのかも含めて、全部吐かせる
        // print $e."\n"; でいいのだけど、出力が見づらいので行頭を少し加工
        print preg_replace('/^/m', '  |', $e)."\n";
    }
}

実行結果

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
  |Exception: 実は・・・後悔しています。罪滅ぼしのため文江を秘書に雇いました in /home/ubuntu/20190227/portopia.php:31
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(23): yamakawa->deepListening()
  |#1 /home/ubuntu/20190227/portopia.php(137): yamakawa->listening()
  |#2 {main}
  |
  |Next Exception: 実は、昔、詐欺をやっていたという過去があります in /home/ubuntu/20190227/portopia.php:25
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(137): yamakawa->listening()
  |#1 {main}
:
(以降長いので省略、最後に全部載せてます)

まとめ

  • 犯人はヤス
  • 重要発言、つまり Exception は聞き漏らさないようにしましょう。
    • catch(Exception $e){ ; } すると、重要発言は何も聞けない。
    • 重要発言を聞くために catch(Exception $e){ print $e->getMessage(); } としても、1つしか聞けない。
    • 重要発言を聞き漏らさない為には catch(Exception $e){ print $e; } とする。
  • PHP版で力尽きました。もし気に入ってもらえたなら、ほかの言語でも事件を解決してみてください:wink:

添付:全ソースコード

portpia.php
<?php

abstract class investigation
{
    abstract function whatName();
    abstract function listening();
    function say($msg) {
        print $this->whatName() . " >>> " . $msg . "\n";
    }
}

class yamakawa extends investigation
{
    function whatName()
    {
        return '山川(殺害された人)  ';
    }

    function listening()
    {
        $this->say("社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!");
        try {
            $this->deepListening();
        } catch (Exception $e) {
            throw new Exception("実は、昔、詐欺をやっていたという過去があります", 0, $e);
        }
    }

    function deepListening()
    {
        throw new Exception("実は・・・後悔しています。罪滅ぼしのため文江を秘書に雇いました");
    }
}

class fumie extends investigation
{
    function whatName()
    {
        return '文江(山川の秘書)    ';
    }

    function listening()
    {
        $this->say("山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!");
        try {
            $this->deepListening();
        } catch (Exception $e) {
            throw new Exception("実は、ヤスは私の兄です", 0, $e);
        }
    }

    function deepListening()
    {
        throw new Exception("実は・・・兄の犯罪に加担していました");
    }
}

class yasu extends investigation
{
    function whatName()
    {
        return 'ヤス(捜査している人)';
    }

    function listening()
    {
        $this->say("山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!");
        try {
            $this->deepListening();
        } catch (Exception $e) {
            throw new Exception("実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります", 0, $e);
        }
    }

    function deepListening()
    {
        throw new Exception("実は・・・詐欺の黒幕は山川社長と知り、復讐のため私が殺害しました");
    }
}

$objs = [];
$objs[] = new yamakawa();
$objs[] = new fumie();
$objs[] = new yasu();

print "\n";
print "***\n";
print "*** 聴取1回目:自己紹介(Exceptionを完全に握りつぶす)\n";
print "***\n";
foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 吐かないよう握りつぶす
        ;
    }
}

print "\n";
print "***\n";
print "*** 聴取2回目:事情を聴く(直近のExceptionのメッセージ1つだけ吐かせる)\n";
print "***\n";
foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 1つだけ吐かせる
        $obj->say($e->getMessage());
    }
}

print "\n";
print "***\n";
print "*** 聴取3回目:真実を吐かせる(ネストしているExceptionのメッセージも含め全部吐かせる)\n";
print "***\n";
foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // 全部吐かせる
        do {
            $obj->say($e->getMessage());
        } while($e = $e->getPrevious());
    }
}

print "\n";
print "***\n";
print "*** 聴取の総括:どこで吐いたのか(Exceptionのメッセージ以外の情報も全部吐かせる)\n";
print "***\n";
foreach ($objs as $obj) {
    print "\n";
    try {
        $obj->listening();
    } catch (Exception $e) {
        // どこで吐いたのかも含めて、全部吐かせる
        // print $e."\n"; でいいのだけど、出力が見づらいので行頭を少し加工
        print preg_replace('/^/m', '  |', $e)."\n";
    }
}

添付:全実行結果

PHP7.2で動作確認しましたが、他のバージョンでも動くと思います。

$ php --version
PHP 7.2.15-0ubuntu0.18.04.1 (cli) (built: Feb  8 2019 14:54:22) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.15-0ubuntu0.18.04.1, Copyright (c) 1999-2018, by Zend Technologies

$ php portopia.php 

***
*** 聴取1回目:自己紹介(Exceptionを完全に握りつぶす)
***

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!

***
*** 聴取2回目:事情を聴く(直近のExceptionのメッセージ1つだけ吐かせる)
***

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
山川(殺害された人)   >>> 実は、昔、詐欺をやっていたという過去があります

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!
文江(山川の秘書)     >>> 実は、ヤスは私の兄です

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!
ヤス(捜査している人) >>> 実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります

***
*** 聴取3回目:真実を吐かせる(ネストしているExceptionのメッセージも含め全部吐かせる)
***

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
山川(殺害された人)   >>> 実は、昔、詐欺をやっていたという過去があります
山川(殺害された人)   >>> 実は・・・後悔しています。罪滅ぼしのため文江を秘書に雇いました

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!
文江(山川の秘書)     >>> 実は、ヤスは私の兄です
文江(山川の秘書)     >>> 実は・・・兄の犯罪に加担していました

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!
ヤス(捜査している人) >>> 実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります
ヤス(捜査している人) >>> 実は・・・詐欺の黒幕は山川社長と知り、復讐のため私が殺害しました

***
*** 聴取の総括:どこで吐いたのか(Exceptionのメッセージ以外の情報も全部吐かせる)
***

山川(殺害された人)   >>> 社長やってます。文江を秘書に雇ってます。ついさっき誰かに殺されました!
  |Exception: 実は・・・後悔しています。罪滅ぼしのため文江を秘書に雇いました in /home/ubuntu/20190227/portopia.php:31
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(23): yamakawa->deepListening()
  |#1 /home/ubuntu/20190227/portopia.php(137): yamakawa->listening()
  |#2 {main}
  |
  |Next Exception: 実は、昔、詐欺をやっていたという過去があります in /home/ubuntu/20190227/portopia.php:25
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(137): yamakawa->listening()
  |#1 {main}

文江(山川の秘書)     >>> 山川社長の秘書です。社長の遺体の第一発見者は私と、社長の甥の川村さんです!
  |Exception: 実は・・・兄の犯罪に加担していました in /home/ubuntu/20190227/portopia.php:54
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(46): fumie->deepListening()
  |#1 /home/ubuntu/20190227/portopia.php(137): fumie->listening()
  |#2 {main}
  |
  |Next Exception: 実は、ヤスは私の兄です in /home/ubuntu/20190227/portopia.php:48
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(137): fumie->listening()
  |#1 {main}

ヤス(捜査している人) >>> 山川社長殺害事件の捜査やってます。社長と揉めていた、山川の甥の川村が怪しい!
  |Exception: 実は・・・詐欺の黒幕は山川社長と知り、復讐のため私が殺害しました in /home/ubuntu/20190227/portopia.php:77
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(69): yasu->deepListening()
  |#1 /home/ubuntu/20190227/portopia.php(137): yasu->listening()
  |#2 {main}
  |
  |Next Exception: 実は、親の会社が詐欺にあい倒産。両親自殺。妹と生き別れの過去があります in /home/ubuntu/20190227/portopia.php:71
  |Stack trace:
  |#0 /home/ubuntu/20190227/portopia.php(137): yasu->listening()
  |#1 {main}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オブジェクト指向とはなんぞや?

なんぞや?

「プログラムの書き方」の考え方のこと。「こう書けば保守性高いよ」というもので、コード量も少なくなる。

保守性?

あとから追加したい機能が出た場合に、修正が楽にできたり、他のところで同じプログラムを使いまわしやすいというものをいう。

所感

そのうちピンとくることを祈る…これだけだと納得感がない。

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

MongoDBを扱うために最低限知っておきたいこと

概要

以下観点でまとめたMongoDBの操作系簡易リファレンスです

  • とりあえずこれだけ知っていればMongoDBをデータストアとしたアプリ開発に入れる
  • 開発Teamメンバーに知っておいてほしいこと

前提

  • MongoDBがセットアップ済みであること
  • 参照、更新コマンド、クエリセレクタは最低限使えること

環境

使用環境 バージョン
CentOS 7.6
MongoDB server 4.0.5

状態確認

データベース一覧

> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB

データベース切り替え(新規作成)

> use testdb
switched to db testdb

コレクション一覧

> show collections
company
department
user

初期化関連

コレクションtruncate

> db.user.remove({})

コレクション削除

> db.user.drop()

データベース削除

drop対象を指定した状態で実行

> use testdb
> db.dropDatabase()

設計・パフォーマンスチューニング

問い合わせプラン確認

> db.user.find({name:"koda"}).explain()

index確認

> db.user.getIndexes()

index作成(通常)

 value値 1:昇順、-1:降順

> db.user.createIndex({name: 1})

index作成(ユニーク)

> db.user.createIndex({name: 1}, {unique: true})

index作成(マルチフィールド)

> db.user.createIndex({name: 1, sex: 1})

ヘルプ・コマンド確認

ヘルプ

> help
        db.help()                    help on db methods
        db.mycoll.help()             help on collection methods
        sh.help()                    sharding helpers
(省略)

コマンド確認

 tab補完が使える

DBコマンド
> db.
db.adminCommand(               db.fsyncLock(                  db.getWriteConcern(            db.revokePrivilegesFromRole(
db.aggregate(                  db.fsyncUnlock(                db.grantPrivilegesToRole(      (省略)
コレクションコマンド
> db.user.
db.user.addIdIfNeeded(              db.user.getCollection(              db.user.mapReduce(
db.user.aggregate(                  db.user.getDB(                      db.user.propertyIsEnumerable
(省略)

コマンドライン実行

直接実行

$ echo $SHELL
/bin/bash

$ mongo --host localhost --quiet testdb --eval 'db.user.find()'
{ "_id" : ObjectId("5c7768915fd5d6ec50b38477"), "uid" : 1, "name" : "koda", "sex" : "男" }
{ "_id" : ObjectId("5c7768915fd5d6ec50b38478"), "uid" : 2, "name" : "mori", "sex" : "男" }
{ "_id" : ObjectId("5c7768915fd5d6ec50b38479"), "uid" : 3, "name" : "kubo", "sex" : "男" }

jsを実行

jsファイルを準備
find.js
var users = db.user.find();
users.forEach(function(user) {
    if (user) {
        print(user._id + '\t' + user.name + '\t' + user.sex);
    }
});
実行
$ mongo --quiet testdb ./find.js
5c7768915fd5d6ec50b38477        koda    男
5c7768915fd5d6ec50b38478        mori    男
5c7768915fd5d6ec50b38479        kubo    男

バックアップ、レストア

ダンプ

$ mongodump --archive=/tmp/testdb.20190228.gz --gzip --db testdb

レストア

対象DBがない状態で実行

> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
$ mongorestore --gzip --archive=/tmp/testdb.20190228.gz --db testdb

エクスポート・インポート

エクスポート

$ mongoexport --db testdb --collection user --out /tmp/user.json

インポート

$ mongoimport --db testdb_new --collection user --file /tmp/user.json

シーケンス

オートインクリメント用の関数を作成しRDBのシーケンスのように利用する

> use testdb

countersコレクションを作成しておく

> db.counters.insert(
   {
      _id: "userid",
      seq: 0
   }
)
> db.counters.find()
{ "_id" : "userid", "seq" : 0 }

オートインクリメント関数を作成

  • 永続的に使える状態にするため、db.system.jsに登録しておく
  • Mongo shell上で定義した変数やfunctionはログオフで消えてしまうため
> db.system.js.save({_id:'my_seq', value:function (name) {
     var ret = db.counters.findAndModify(
            {
              query: { _id: name },
              update: { $inc: { seq: 1 } },
              new: true
            }
     );
     return ret.seq;
  }
});

確認のためにログオフ

> exit
bye

再接続

$ mongo
> use testdb

登録したスクリプトをロード

> db.loadServerScripts()

シーケンス取得

> my_seq('userid')
1
> my_seq('userid')
2
> my_seq('userid')
3

実際に使う場合はこんな感じ

> db.users.insert({_id: my_seq('userid'), name: "seq_test_user"} )
> db.users.find()
{ "_id" : 4, "name" : "seq_test_user" }

ユーザ認証

スーパーユーザを準備したうえでアプリ用ユーザを作成するイメージ

ユーザ管理者の登録

/etc/mongod.conf
#security:
> use admin
> db.createUser({
  user:"admin",
  pwd:"manager",
  roles:[{ role:"userAdminAnyDatabase", db:"admin" }]
})

再起動

認証設定を有効にする

/etc/mongod.conf
security:
  authorization: enabled
# systemctl restart mongod

アプリ用ユーザ登録

ユーザ管理者で再接続

$ mongo -u "admin" -p "manager" --authenticationDatabase="admin"
> use testdb
> db.createUser(
  {
    user: "airuser",
    pwd:"airuser",
    roles:[
       {role:"readWrite",  db:"testdb"}
    ]
  }
)

cf. Built-In Roles

ロール変更

> use testdb
> db.updateUser(
  "airuser",
  {
    roles:[
      {role: "readWrite",db: "testdb"}
    ]
  }
)

登録済みユーザ確認

> use admin
> db.system.users.find()

接続確認

アプリ用ユーザで接続

$ mongo --host localhost -u "airuser" -p "airuser" --authenticationDatabase="testdb"

許可DBだけ見える

> show dbs
testdb  0.000GB

おまけ

PHPでアクセス

ユーザ認証ありの状態で検証

ファイル準備
mongo.php
<?php
// DBへ接続
$mongo = new MongoClient("mongodb://localhost:27017/testdb",
         array("username" => "airuser", "password" => "airuser"));
// データベースを指定
$db = $mongo->selectDB("testdb");
// コレクションを指定
$coll = $db->selectCollection("user");
// コレクションのドキュメントを取得
$docs = $coll->find(array('uid' => 1));
// 表示
foreach ($docs as $id => $obj) {
    print_r($obj);
}
?>
実行
$ php mongo.php
Array
(
    [_id] => MongoId Object
        (
            [$id] => 5c7787e73cf5a95585ea27f7
        )

    [uid] => 1
    [name] => koda
    [sex] => 男
)

フィールドを縦に展開

利用頻度が高いですよね

> db.user.find().pretty()
{
        "_id" : ObjectId("5c7768915fd5d6ec50b38477"),
        "uid" : 1,
        "name" : "koda",
        "sex" : "男"
}
  • cf.
    • MySQL: SQLの最後に \G
    • PostgreSQL: メタコマンド \x

参考

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

switch文の比較、JavaScriptは'==='、PHPは’==’

同じswitch文を使って出力結果の違いを比較しよう

下はJSのコードですが、PHPで考える場合は読み替えてください。すなわち、num$numconsole.log(~)echo ~と読み替えてください。

JavaScriptとPHP
// A
switch(num) {
  case 5:
    console.log('整数の5');  
    break;
  default:
    console.log('該当なし');
    break;
}

// B
switch(num) {
  case '5':
    console.log('文字列の5');
    break;
  default:
    console.log('該当なし');
    break;
}

結果

155081659966.jpg

なぜこうなるのか

式を比較するときに、JavaScriptは厳密等価演算子(===)を使った厳密な比較を行い、PHPは等価演算子(==)を使った緩やかな比較を行うから。

JavaScriptのソース

switch文はまず始めに式を評価します。次に (厳密等価演算子===を使用して) 式が入力式の結果と評価される値が等しい最初の case 節を探し、…
 
switch - JavaScript | MDN

PHPのソース

switch/case が行うのは、 緩やかな比較 であることに注意しましょう。
PHP: switch - Manual

PHPでも厳密に比較したい

PHP
// A
switch(true) {
  case $num === 5:
    echo '整数の5';  
    break;
  default:
    echo '該当なし';
    break;
}

// B
switch(true) {
  case $num === '5':
    echo '文字列の5';
    break;
  default:
    echo '該当なし';
    break;
}

このように書くと厳密な比較になります。結果は以下の通り。なお、「JavaScript」と「PHP」の結果は、同じswitch文を使って出力結果の違いを比較しようのところで紹介しかコードを使ったときの結果で、「PHP(厳密)」はここで紹介したコードを使ったときの結果です。
155081659966.jpg

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

PHPで日本語の曜日を使いたい

class MyDateTime extends \DateTime
{
    public function format ($f) {
        $s = parent::format($f);
        $daynames = ['火', '水', '木', '金', '土', '日', '月'];
        $dayname = $daynames[parent::format('w')];
        return str_replace('x', $dayname, $s);
    }
}
print (new MyDateTime())->format('Ymd(x)');

簡単だけどコードがぐっとシンプルになってとても便利なのでよく使う
多言語対応も簡単

class MyDateTime extends \DateTime
{
    const DAYNAME_SET = [
        'ja' => ['日', '月', '火', '水', '木', '金', '土'],
        'ru' => ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб']
    ];

    public function format ($f, $lang = 'ja') {
        $s = parent::format($f);
        $daynames = self::DAYNAME_SET[$lang];
        $dayname = $daynames[parent::format('w')];
        return str_replace('x', $dayname, $s);
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PhpStorm(IntelliJ IDEA)でLaravelプロジェクトの新規作成からローカルデバッグ環境構築まで

動作環境

Windows 7
IntelliJ IDEA 2018.3.4
PHP 7.3.2
Xdebug v2.7.0RC2
Laravel Framework 5.8.2

前提条件

  • IntelliJ IDEAにPHPプラグインがインストールされていること

背景

普段Unity+Riderで開発をしています
デバッグ時にはデバッグボタン押すだけでブレークポイントが有効になりステップ実行が可能になります
効率的にデバッグをこなす上でブレークポイントの有効可は必須なので今回の環境構築を行いました
1クリックでローカルサーバの起動→デバッグ開始できるようにします

PHPのインストール

PHP download
- Cドライブ直下に展開 (C:\php-7.3.2)
- php.ini-developmentをコピーしてphp.iniにリネーム
- php.iniないの下記設定のコメントアウトを解除

;extension_dir = "ext"
;extension=openssl
;extension=mbstring
;extension=pdo_mysql

Xdebugのインストール

Xdebug

  • php --infoで出力される情報をコピーして上記サイトに貼り付けAnalyse my phpinfo() outputをクリック
  • 環境に合わせたxdebugのダウンロードリンクが出てくるのでダウンロード(自分の環境ではphp_xdebug-2.7.0RC2-7.3-vc15-x86_64.dll)
  • C:\php-7.3.2\ext配下にコピー
  • php.iniにzend_extension = php_xdebug-2.7.0RC2-7.3-vc15-x86_64.dllを追記

  • ext\php_xdebug~にするとデバッグ時にエラーが出たのでextは抜きました

PhpStorm(IntelliJ IDEA)でLaravelプロジェクトの新規作成

  • Create New Project -> PHP Composer Project
    2.png

  • Project name: laravel-sample

  • composer.pharにチェック

  • Download composer.pharにチェック

  • PHP Interpriter: PHP(7.3.2)を選択

  • Filter package: laravel/laravelを入力して一覧から選択してFinish

3.png

起動確認

php artisan key:generate # APP_KEYの生成
php artisan serve

でローカルサーバを起動しlocalhost:8000にアクセスして起動できることを確認します。

Built-in Web Serverの設定

php artisan serveでローカルサーバ起動しましたがXdebugを使ってデバッグでするためPhpStormで設定を行います

  • Run -> Edit Configurations...
  • PHP Built-in Web Serverを追加 - Name: Build-in Web Server - Host: localhost - Port: 8000 - Document root:<プロジェクトのディレクトリ>/public - Interpreter options: -dxdebug.remote_enable=1 -dxdebug.remote_autostart=1 -dxdebug.remote_host=localhost -dxdebug.remote_port=9000 10.png

xdebugの設定

  • File -> Settings -> PHP

    • CLI Interpriter: PHP(7.3.2) 5.png
  • File -> Settings -> PHP -> Debug -> Xdegug

    • Debug port: 9000
  • 6.png

  • File -> Settings -> PHP -> Servers

    • 「+」ボタンでServer追加
    • Name: localhost
    • Host: localhost
    • Port: 8000
    • Debugger: xdebug
    • Use path mappingsにチェック
    • 作成したプロジェクトのルートディレクトリのAbsolute path on the Serverに同じパスを入力 8.png

デバッグ実行

  • Run -> RunでBuilt-in Web Serverで設定したサーバを選択して実行

または

  • 赤枠のマークをクリックして実行 11.png

ブレークポイントを仕掛けてhttp://localhost:8000にアクセスすると下の画像のようにステップ実行が可能となっているはずです
12.png

受話器のマークが赤枠のようになっていないことに気をつけてください
13.png

良いデバッグライフを(๑•̀ㅂ•́)و✧

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

【laravel5.7】MultiAuthにパスワードリセットを追加する

MultiAuthにパスワードリセットを追加する実装メモ
【laravel5.7】 MultiAuthログインの続きとして書きます。

やりたいこと

User/Adminそれぞれにパスワードリセットを実装したい

環境

MacOS 10.14.3
VisualStudio
laravel 5.7

Admin側の設定

auth.php

リセット用のtokenを保存するテーブルが必要なので、auth.phpで設定します。

config/auth.php
'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
        ],

+        'admins' => [
+            'provider' => 'admins',
+            'table' => 'password_resets',
+            'expire' => 60,
+        ],
 ],

Controller

ForgotPasswordController.php

app/Http/Controllers/Admin/Auth/ForgetPasswordController.php
<?php

+namespace App\Http\Controllers\Admin\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;

class ForgotPasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset emails and
    | includes a trait which assists in sending these notifications from
    | your application to your users. Feel free to explore this trait.
    |
    */

    use SendsPasswordResetEmails;

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

+    public function showLinkRequestForm()
+    {
+        return view('admin.auth.passwords.email');
+    }

+    protected function guard()
+    {
+        return \Auth::guard('admin');
+    }

+    public function broker()
+    {
+        return \Password::broker('admins');
+    }
}

ResetPasswordController.php

app/Http/Controllers/Admin/Auth/ResetPasswordController.php
<?php

+namespace App\Http\Controllers\Admin\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
+use Illuminate\Http\Request;


class ResetPasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset requests
    | and uses a simple trait to include this behavior. You're free to
    | explore this trait and override any methods you wish to tweak.
    |
    */

    use ResetsPasswords;

    /**
     * Where to redirect users after resetting their password.
     *
     * @var string
     */
+    protected $redirectTo = '/admin/home';

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

+    public function showResetForm(Request $request, $token = null)
+    {
+        return view('admin.auth.passwords.reset')->with(['token' => $token,'email' => $request->email]);
+    }

+    protected function guard()
+    {
+        return \Auth::guard('admin');
+    }

+    public function broker()
+    {
+        return \Password::broker('admins');
+    }
}

Views

email.blade.php

resources/views/admin/auth/passwords/email.blade.php
+@extends('layouts.admin.auth')

@section('auth')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card mx-4">
                <div class="card-body p-4">
                    <h2>{{ __('Reset Password') }}</h2>
                    <p class="text-muted">リセットURLを送信します。</p>
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif

+                    <form method="POST" action="{{ route('admin.password.email') }}">
                    @csrf
                    <div class="col-auto">

                        <div class="input-group mb-3">
                            <div class="input-group-prepend">
                                <div class="input-group-text">@</div>
                            </div>
                            <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>
                        </div>
                        @if ($errors->has('email'))
                            <span class="invalid-feedback" role="alert">
                                <strong>{{ $errors->first('email') }}</strong>
                            </span>
                        @endif

                        <button type="submit" class="btn btn-primary">
                            {{ __('Send Password Reset Link') }}
                        </button>
                    </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

reset.blade.php

resources/views/admin/auth/passwords/reset.blade.php
+@extends('layouts.admin.auth')

@section('auth')
<div class="container">
        <div class="row justify-content-center">
            <div class="col-md-6">
                <div class="card mx-4">
                    <div class="card-body p-4">
                        <h2>{{ __('Reset Password') }}</h2>

                        {{-- <div class="card-body"> --}}
+                            <form method="POST" action="{{ route('admin.password.update') }}">
                                @csrf

                                <input type="hidden" name="token" value="{{ $token }}">
                                <div class="col-auto">

                                    <div class="input-group mb-3">
                                        <div class="input-group-prepend">
                                            <div class="input-group-text">Email</div>
                                        </div>
                                        <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ $email ?? old('email') }}" required autofocus>
                                    </div>
                                    @if ($errors->has('email'))
                                        <span class="invalid-feedback" role="alert">
                                            <strong>{{ $errors->first('email') }}</strong>
                                        </span>
                                    @endif

                                </div>

                                <div class="col-auto">

                                    <div class="input-group mb-3">
                                        <div class="input-group-prepend">
                                            <div class="input-group-text"><i class="fas fa-key"></i></div>
                                        </div>
                                        <input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>

                                        @if ($errors->has('password'))
                                            <span class="invalid-feedback" role="alert">
                                                <strong>{{ $errors->first('password') }}</strong>
                                            </span>
                                        @endif
                                    </div>

                                </div>

                                <div class="col-auto">

                                    <div class="input-group mb-3">
                                        <div class="input-group-prepend">
                                            <div class="input-group-text"><i class="fas fa-key"></i></div>
                                        </div>
                                        <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
                                    </div>
                                </div>

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

ルーティング

web.php

routes/web.php
// Admin
Route::group(['namespace' => 'Admin','prefix'=>'admin'],function(){
    // home
    Route::get('home','HomeController@index')->name('admin.home');

    // login logout
    Route::get('login','Auth\LoginController@showLoginForm')->name('admin.login');
    Route::post('login','Auth\LoginController@login')->name('admin.login');
    Route::post('logout','Auth\LoginController@logout')->name('admin.logout');

    // register
    Route::get('register','Auth\RegisterController@showRegisterForm')->name('admin.register');
    Route::post('register','Auth\RegisterController@register')->name('admin.register');

+    // password resets
+    Route::post('password/email','Auth\ForgotPasswordController@sendResetLinkEmail')->name('admin.password.email');
+    Route::post('password/reset','Auth\ResetPasswordController@reset')->name('admin.password.request');
+    Route::get('password/reset','Auth\ForgotPasswordController@showLinkRequestForm')->name('admin.password.update');
+    Route::get('password/reset/{token}','Auth\ResetPasswordController@showResetForm')->name('admin.password.reset');
});

送信機能のオーバーライド

AdminPasswordResetNotification.phpを作ります。

terminal
php artisan make:notification AdminPasswordResetNotification
app/Notifications/AdminPasswordResetNotification.php
<?php

namespace App\Notifications;

use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;
use Illuminate\Notifications\Messages\MailMessage;

class AdminPasswordResetNotification extends ResetPasswordNotification
{
    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->line('ADMINパスワードリセット用メール.')
            ->action('Reset Password', url(config('url').route('admin.password.reset', $this->token, false)))
            ->line('If you did not request a password reset, no further action is required.');
    }
}

Admin.phpの変更

AdminPasswordResetNotificationがメールで送信されるようにModelに設定します。

app/Admin.php
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

+use App\Notifications\AdminPasswordResetNotification;

class Admin extends Authenticatable 
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password','api_token'
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

+    public function sendPasswordResetNotification($token)
+    {
+        $this->notify(new AdminPasswordResetNotification($token));
+    }
}

動作確認

http://127.0.0.1:8000/admin/login

web
スクリーンショット 2019-02-28 12.56.33.png

入力したメールアドレス
スクリーンショット 2019-02-28 12.56.54.png

Reset Passwordボタンをクリック
スクリーンショット 2019-02-28 13.07.58.png

こんな感じで動けばOK

User側の設定

Adminとやることは変わりません

Controller

ForgotPasswordController.php

app/Http/Controllers/User/Auth/ForgetPasswordController.php
<?php

+namespace App\Http\Controllers\User\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;

class ForgotPasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset emails and
    | includes a trait which assists in sending these notifications from
    | your application to your users. Feel free to explore this trait.
    |
    */

    use SendsPasswordResetEmails;

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

+    public function showLinkRequestForm()
+    {
+        return view('user.auth.passwords.email');
+    }

+    protected function guard()
+    {
+        return \Auth::guard('user');
+    }

+    public function broker()
+    {
+        return \Password::broker('users');
+    }
}

ResetPasswordController.php

app/Http/Controllers/User/Auth/ResetPasswordController.php
<?php

+namespace App\Http\Controllers\User\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
+use Illuminate\Http\Request;


class ResetPasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset requests
    | and uses a simple trait to include this behavior. You're free to
    | explore this trait and override any methods you wish to tweak.
    |
    */

    use ResetsPasswords;

    /**
     * Where to redirect users after resetting their password.
     *
     * @var string
     */
+    protected $redirectTo = '/user/home';

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

+    public function showResetForm(Request $request, $token = null)
+    {
+        return view('user.auth.passwords.reset')->with(['token' => $token,'email' => $request->email]);
+    }

+    protected function guard()
+    {
+        return \Auth::guard('user');
+    }

+    public function broker()
+    {
+        return \Password::broker('users');
+    }
}

Views

email.blade.php

resources/views/user/auth/passwords/email.blade.php
+@extends('layouts.user.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Reset Password') }}</div>

                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif

+                    <form method="POST" action="{{ route('user.password.email') }}">
                        @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{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>

                                @if ($errors->has('email'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Send Password Reset Link') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

reset.blade.php

resources/views/user/auth/passwords/reset.blade.php
+@extends('layouts.user.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Reset Password') }}</div>

                <div class="card-body">
+                    <form method="POST" action="{{ route('user.password.update') }}">
                        @csrf

                        <input type="hidden" name="token" value="{{ $token }}">

                        <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{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ $email ?? old('email') }}" required autofocus>

                                @if ($errors->has('email'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </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{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>

                                @if ($errors->has('password'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('password') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

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

                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
                            </div>
                        </div>

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

ルーティング

web.php

routes/web.php
// User
Route::group(['namespace' => 'User','prefix'=>'user'],function(){
    Route::get('/',function(){
        return redirect()->to('user/home');
    })->name('user');
    // home
    Route::get('home','HomeController@index')->name('user.home');

    // login lgoout
    Route::get('login','Auth\LoginController@showLoginForm')->name('user.login');
    Route::post('login','Auth\LoginController@login')->name('user.login');
    Route::post('logout','Auth\LoginController@logout')->name('user.logout');

    // register
    Route::get('register','Auth\RegisterController@showRegisterForm')->name('user.register');
    Route::post('register','Auth\RegisterController@register')->name('user.register');

    // emailverify
    Route::middleware('throttle:6,1')->get('email/resend','Auth\VerificationController@resend')->name('user.verification.resend');
    Route::middleware('throttle:6,1')->get('email/verify','Auth\VerificationController@show')->name('user.verification.notice');
    Route::middleware('signed')->get('email/verify/{id}','Auth\VerificationController@verify')->name('user.verification.verify');


+    // password reset
+    Route::post('password/email','Auth\ForgotPasswordController@sendResetLinkEmail')->name('user.password.email');
+    Route::post('password/reset','Auth\ResetPasswordController@reset')->name('user.password.request');
+    Route::get('password/reset','Auth\ForgotPasswordController@showLinkRequestForm')->name('user.password.update');
+    Route::get('password/reset/{token}','Auth\ResetPasswordController@showResetForm')->name('user.password.reset');
});

送信機能のオーバーライド

UserPasswordResetNotification.phpを作ります。

terminal
php artisan make:notification UserPasswordResetNotification
app/Notifications/UserPasswordResetNotification.php
<?php

namespace App\Notifications;

use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;
use Illuminate\Notifications\Messages\MailMessage;

class UserPasswordResetNotification extends ResetPasswordNotification
{
    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->line('USERパスワードリセット用メール.')
            ->action('Reset Password', url(config('url').route('user.password.reset', $this->token, false)))
            ->line('If you did not request a password reset, no further action is required.');
    }
}

User.phpの変更

UserPasswordResetNotificationがメールで送信されるようにModelに設定します。

app/User.php
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

+use App\Notifications\UserPasswordResetNotification;

class User extends Authenticatable implements MustVerifyEmail
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

+    public function sendPasswordResetNotification($token)
+    {
+        $this->notify(new UserPasswordResetNotification($token));
+    }
}

動作確認

http://127.0.0.1:8000/user/login

Adminと同じように動けばOKです。

メール内容
スクリーンショット 2019-02-28 12.57.22.png

以上です。

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

【laravel5.7】 MultiAuthにEmailVerification

MultiAuthにEmailVerificationを対応させる実装メモ。
【laravel5.7】 MultiAuthログインの続きとして書きます。

MultiAuthなしの実装メモは別記事で書いているので、参考になれば、、、
【laravel5.7】 Email Verificationの使い方

やりたいこと

MultiAuth(Admin/User)のUserのみにEmail Verificationを対応させたい。

環境

MacOS 10.14.3
VisualStudio
laravel 5.7

Email Verificationを使えるようにする

MustVerifyEmailの追加

app/User.php
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

+class User extends Authenticatable implements MustVerifyEmail
{

ルーティング設定

routes/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!
|
*/

use Illuminate\Http\Request;

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

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');
// User
Route::group(['namespace' => 'User','prefix'=>'user'],function(){
+    Route::get('/',function(){
+        return redirect()->to('user/home');
+    })->name('user');
    // home
    Route::get('home','HomeController@index')->name('user.home');

    // login lgoout
    Route::get('login','Auth\LoginController@showLoginForm')->name('user.login');
    Route::post('login','Auth\LoginController@login')->name('user.login');
    Route::post('logout','Auth\LoginController@logout')->name('user.logout');

    // register
    Route::get('register','Auth\RegisterController@showRegisterForm')->name('user.register');
    Route::post('register','Auth\RegisterController@register')->name('user.register');

+    // emailverify
+    Route::middleware('throttle:6,1')->get('email/resend','Auth\VerificationController@resend')->name('user.verification.resend');
+    Route::middleware('throttle:6,1')->get('email/verify','Auth\VerificationController@show')->name('user.verification.notice');
+    Route::middleware('signed')->get('email/verify/{id}','Auth\VerificationController@verify')->name('user.verification.verify');
});

// Admin
以下省略

単体ログインのEmailVerificationのAuth:routes(['verify'=>true]);
MultiAuthでは上の(//emailverify以下の部分)のように一つずつ書いていきます。

VerificationController.php

app/Http/Controllers/User/Auth/VerificationController.php
<?php

+namespace App\Http\Controllers\User\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;
+use Illuminate\Http\Request;


class VerificationController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Email Verification Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling email verification for any
    | user that recently registered with the application. Emails may also
    | be re-sent if the user didn't receive the original email message.
    |
    */

    use VerifiesEmails;

    /**
     * Where to redirect users after verification.
     *
     * @var string
     */
+    protected $redirectTo = '/user/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
+        $this->middleware('auth:user');
        $this->middleware('signed')->only('verify');
        $this->middleware('throttle:6,1')->only('verify', 'resend');
    }

+    public function show(Request $request)
+    {
+        return $request->user()->hasVerifiedEmail()
+                        ? redirect($this->redirectPath())
+                        : view('user.auth.verify');
+    }
}

HomeController.php

app/Http/Controllers/User/HomeController.php
    public function __construct()
    {
        $this->middleware('auth:user');
+       $this->middleware('allVerified');
    }

Middleware

AllEnsureEmailIsVerified.phpを作成します

terminal
php artisan make:middleware AllEnsureEmailIsVerified
app/Http/Middleware/AllEnsureEmailIsVerified.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Auth;

class AllEnsureEmailIsVerified
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $guards = array_keys(config('auth.guards'));

        foreach($guards as $guard) {
            if($guard == 'user') {
                if (Auth::guard($guard)->check()) {
                    if (! Auth::guard($guard)->user() ||
                        (Auth::guard($guard)->user() instanceof MustVerifyEmail &&
                        ! Auth::guard($guard)->user()->hasVerifiedEmail())) {
                        // dd('ddd');
                        return $request->expectsJson()
                                ? abort(403, 'Your email address is not verified.')
                                : Redirect::route('user.verification.notice');
                    }  
                }
            }
        }
        return $next($request);
    }
}

Kernel.php

app/Http/Kernel.php
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+       'allVerified' => \App\Http\Middleware\AllEnsureEmailIsVerified::class,
    ];

メール内容をカスタマイズ

Notificationsの作成

terminal
php artisan make:notification UserVerifyEmailNotification
app/Notifications/UserVerifyEmailNotification.php
<?php

namespace App\Notifications;

use Illuminate\Auth\Notifications\VerifyEmail as VerifyEmailNotification;
use Illuminate\Notifications\Messages\MailMessage;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Lang;

class UserVerifyEmailNotification extends VerifyEmailNotification
{
    public function toMail($notifiable)
    {
        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable);
        }

        return (new MailMessage)
            ->subject(Lang::getFromJson('USERメール確認'))
            ->line(Lang::getFromJson('クリックして認証してください.'))
            ->action(
                Lang::getFromJson('メール認証'),
                $this->verificationUrl($notifiable)
            )
            ->line(Lang::getFromJson('もしこのメールに覚えが無い場合は破棄してください。'));
    }


    protected function verificationUrl($notifiable)
    {
        return URL::temporarySignedRoute(
            'user.verification.verify', Carbon::now()->addMinutes(60), ['id' => $notifiable->getKey()]
        );
    }
}

User.php

UserVerifyEmailNotificationがメールで送信されるようにModelに設定します。

app/User.php
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

+use App\Notifications\UserVerifyEmailNotification;

class User extends Authenticatable implements MustVerifyEmail
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

+    public function sendEmailVerificationNotification()
+    {
+        $this->notify(new UserVerifyEmailNotification);
+    }
}

動作確認

User
http://127.0.0.1:8000/user/register から新しくアカウントを登録します。

web上
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3235323636372f31393034326236312d343935662d333839622d666337352d3834326263313839616439312e706e67.png

登録したメールアドレス
スクリーンショット 2019-02-28 11.48.51.png

メール認証ボタンクリックすると、
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3235323636372f37653335643530392d353037382d663966382d653163332d6335396463303931653936382e706e67.png

こうなれば成功です。
まだ英語の部分があるので、その辺の設定は後々設定しようと思います。

Adminでも実装したい場合はこれと同様に編集すればOK。
とりあえず以上です。

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

【laravel5.7】 MultiAuthにEmailVerificationを対応させる

MultiAuthにEmailVerificationを対応させる実装メモ。
【laravel5.7】 MultiAuthログインの続きとして書きます。

MultiAuthなしの実装メモは別記事で書いているので、参考になれば、、、
【laravel5.7】 Email Verificationの使い方

やりたいこと

MultiAuth(Admin/User)のUserのみにEmail Verificationを対応させたい。

環境

MacOS 10.14.3
VisualStudio
laravel 5.7

Email Verificationを使えるようにする

MustVerifyEmailの追加

app/User.php
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

+class User extends Authenticatable implements MustVerifyEmail
{

ルーティング設定

routes/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!
|
*/

use Illuminate\Http\Request;

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

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');
// User
Route::group(['namespace' => 'User','prefix'=>'user'],function(){
+    Route::get('/',function(){
+        return redirect()->to('user/home');
+    })->name('user');
    // home
    Route::get('home','HomeController@index')->name('user.home');

    // login lgoout
    Route::get('login','Auth\LoginController@showLoginForm')->name('user.login');
    Route::post('login','Auth\LoginController@login')->name('user.login');
    Route::post('logout','Auth\LoginController@logout')->name('user.logout');

    // register
    Route::get('register','Auth\RegisterController@showRegisterForm')->name('user.register');
    Route::post('register','Auth\RegisterController@register')->name('user.register');

+    // emailverify
+    Route::middleware('throttle:6,1')->get('email/resend','Auth\VerificationController@resend')->name('user.verification.resend');
+    Route::middleware('throttle:6,1')->get('email/verify','Auth\VerificationController@show')->name('user.verification.notice');
+    Route::middleware('signed')->get('email/verify/{id}','Auth\VerificationController@verify')->name('user.verification.verify');
});

// Admin
以下省略

単体ログインのEmailVerificationのAuth:routes(['verify'=>true]);
MultiAuthでは上の(//emailverify以下の部分)のように一つずつ書いていきます。

VerificationController.php

app/Http/Controllers/User/Auth/VerificationController.php
<?php

+namespace App\Http\Controllers\User\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;
+use Illuminate\Http\Request;


class VerificationController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Email Verification Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling email verification for any
    | user that recently registered with the application. Emails may also
    | be re-sent if the user didn't receive the original email message.
    |
    */

    use VerifiesEmails;

    /**
     * Where to redirect users after verification.
     *
     * @var string
     */
+    protected $redirectTo = '/user/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
+        $this->middleware('auth:user');
        $this->middleware('signed')->only('verify');
        $this->middleware('throttle:6,1')->only('verify', 'resend');
    }

+    public function show(Request $request)
+    {
+        return $request->user()->hasVerifiedEmail()
+                        ? redirect($this->redirectPath())
+                        : view('user.auth.verify');
+    }
}

HomeController.php

app/Http/Controllers/User/HomeController.php
    public function __construct()
    {
        $this->middleware('auth:user');
+       $this->middleware('allVerified');
    }

Middleware

AllEnsureEmailIsVerified.phpを作成します

terminal
php artisan make:middleware AllEnsureEmailIsVerified
app/Http/Middleware/AllEnsureEmailIsVerified.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Auth;

class AllEnsureEmailIsVerified
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $guards = array_keys(config('auth.guards'));

        foreach($guards as $guard) {
            if($guard == 'user') {
                if (Auth::guard($guard)->check()) {
                    if (! Auth::guard($guard)->user() ||
                        (Auth::guard($guard)->user() instanceof MustVerifyEmail &&
                        ! Auth::guard($guard)->user()->hasVerifiedEmail())) {
                        // dd('ddd');
                        return $request->expectsJson()
                                ? abort(403, 'Your email address is not verified.')
                                : Redirect::route('user.verification.notice');
                    }  
                }
            }
        }
        return $next($request);
    }
}

Kernel.php

app/Http/Kernel.php
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+       'allVerified' => \App\Http\Middleware\AllEnsureEmailIsVerified::class,
    ];

verify.blade.php

views/user/auth/verify.blade.php
+@extends('layouts.user.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('メールアドレスの認証') }}</div>

                <div class="card-body">
                    @if (session('resent'))
                        <div class="alert alert-success" role="alert">
                            {{ __('認証メールを再送信しました。') }}
                        </div>
                    @endif

                    {{ __('メールアドレスの認証をしてください。') }}
+                    {{ __('もしメールを受け取ってないなら、<a href="{{ route('user.verification.resend') }}">ここをクリックしてください</a>。') }}
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

メール内容をカスタマイズ

Notificationsの作成

terminal
php artisan make:notification UserVerifyEmailNotification
app/Notifications/UserVerifyEmailNotification.php
<?php

namespace App\Notifications;

use Illuminate\Auth\Notifications\VerifyEmail as VerifyEmailNotification;
use Illuminate\Notifications\Messages\MailMessage;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Lang;

class UserVerifyEmailNotification extends VerifyEmailNotification
{
    public function toMail($notifiable)
    {
        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable);
        }

        return (new MailMessage)
            ->subject(Lang::getFromJson('USERメール確認'))
            ->line(Lang::getFromJson('クリックして認証してください.'))
            ->action(
                Lang::getFromJson('メール認証'),
                $this->verificationUrl($notifiable)
            )
            ->line(Lang::getFromJson('もしこのメールに覚えが無い場合は破棄してください。'));
    }


    protected function verificationUrl($notifiable)
    {
        return URL::temporarySignedRoute(
            'user.verification.verify', Carbon::now()->addMinutes(60), ['id' => $notifiable->getKey()]
        );
    }
}

User.php

UserVerifyEmailNotificationがメールで送信されるようにModelに設定します。

app/User.php
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

+use App\Notifications\UserVerifyEmailNotification;

class User extends Authenticatable implements MustVerifyEmail
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

+    public function sendEmailVerificationNotification()
+    {
+        $this->notify(new UserVerifyEmailNotification);
+    }
}

動作確認

User
http://127.0.0.1:8000/user/register から新しくアカウントを登録します。

web
スクリーンショット 2019-03-01 11.27.52.png

登録したメールアドレス
スクリーンショット 2019-02-28 11.48.51.png

メール認証ボタンクリックすると、
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3235323636372f37653335643530392d353037382d663966382d653163332d6335396463303931653936382e706e67.png

こうなれば成功です。
まだ英語の部分があるので、その辺の設定は後々設定しようと思います。

Adminでも実装したい場合はこれと同様に編集すればOK。
とりあえず以上です。

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

Php Inspections EA Extended 3.0.11/Ultimate 2.0.13の新機能

Php Inspections (EA Extended)Php Inspections (EA Ultimate)はPhpStormのInspection(コード検査)機能をさらに強力なものに拡張してくれるPHPerなら絶対に入れておきたいプラグインです。無償で使えるのがEA Extendedで、それに更に検査ルールを加えたハイエンドの有償版がEA Ultimateです。

本稿では、そのEA Extended 3.0.11(2019/2/23リリース)、EA Ultimate 2.0.13(2019/2/25リリース)に追加された新たなチェックツールを紹介します。

EA Extended/EA Ultimate共通の新機能

PHPUnitのバグとベストプラクティス

検査名: PhpUnit: bugs and best practices

PhpUnit関連のコードを分析してバグを報告し、ベストプラクティスを使用することを提案します。単体テストでの型安全性を強化するために、インスペクションの設定で「assertSameの使用を提案」を有効にすることができます。

php-playground___Volumes_dev_php-playground__-_____ExampleTest_php__php-playground_.png

assertEqualsよりassertSameのほうが良く、さらにこの場合、assertSameよりもassertTrueのほうが良いということを指摘してくれます。

2019-02-28 11.22.31.gif

array_pushの誤用

検査名: 'array_push(...)' misused

array_push(...) 呼び出しが $array[] = ... と同じ意味で使われている場合に、$array[] =に置き換えることを提案します。そのほうが関数を呼び出すより2倍高速なためです。

array_pushの誤用を検出する様子:

php-playground___Volumes_dev_php-playground__-_____ExampleTest_php__php-playground_.png

誤用は自動で修正してくれる:

php-playground___Volumes_dev_php-playground__-_____ExampleTest_php__php-playground_.png

EA Ultimate限定の新機能

if-return-returnの単純化

検査名: If-return-return could be simplified

条件に基づいてbool値を返すif()構造を分析し、代わりに条件を返すように単純化します。

    if ($conditions) {
        return true;
    }

    return false;

上のコードは下のように単純化できます:

    return $conditions;

繰り返しのメソッド呼び出し

検査名: Repetitive method calls

ループでの繰り返しメソッド呼び出しを報告します。 呼び出し量を減らすために、ローカル変数化を提案します。

疑わしいバイナリオペレーション

検査名: Suspicious binary operations

検査は、instanceof演算子など不明確で疑わしい演算子の優先順位、誤ったオペレータ、ハードコードされたブール値、その他のバグを含む可能性があるケースなど、さまざまなケースを報告します。

$a && $b || $c($a && $b) || $cと書いたほうがいいよと提案してくれる様子:

php-playground___Volumes_dev_php-playground__-_____ExampleTest_php__php-playground_.png

疑わしい変数宣言

検査名: Suspicious variable declaration

疑わしい変数の宣言を分析して報告します。

不要なcontinue

検査名: Unnecessary continue statements

削ってもいいcontinueがあれば教えてくれます。

php-playground___Volumes_dev_php-playground__-_____ExampleTest_php__php-playground_.png

関連

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

【php/Laravel】Carbon使って「来月の今日」を求めたいのです

Carbonとは

まず公式をお読みください。
https://carbon.nesbot.com/

よくわからん、という場合はこの記事自体読む必要ないと思いますけど、ググって実際の利用ケースを参照してもらうと良いと思います。

何が便利なのか

phpの組み込み関数だけで日付計算しようとすると、結構煩雑な記述になって、後から見た時何しようとしてんのかわからん、っていうのが一番の問題ですよね。
たとえば、1836年1月3日に生まれた坂本龍馬が今生きてたら何歳かな?とか計算しようとしたら・・・

誕生日の計算
$birthday = "1836-01-03";    // なんかデータベースにでも入ってると仮定して
   :
   :
$now = date('Ymd');
$bd = date('Ymd', strtotime($birthday));
$age = floor(($now-$bd)/10000);

# [結果]
# => 183.0

のようになります。
特にこの、floor(($now-$bd)/10000) とかの計算部分がよくわからんですね。
こんなもん標準の関数で用意しとけ、て思うくらいの定型文。
なので、これをCarbon使ってやると

Carbonで誕生日から年齢計算
use Carbon\Carbon;
  :
$birthday = "1836-01-03";
  :
$age = Carbon::parse($birthday)->age;

# [結果]
# => 183

なんと、日付から生成したオブジェクトのageプロパティにアクセスしたら、自動的に年齢が取れてしまいました。

こういう細かく行き届いた機能が山のように用意されています。
便利ですね!

これまで(個人的に)問題だったこと

実務でlaravelを使ってシステム組んでいて、日付計算にcarbonを使うようになってずいぶんコード量もバグも減ったと思うのですが、1個だけどうしても解決できない面倒な記述があったのです。

それは 来月 の日付取得。

普通に考えれば、「来月の今日」をもってこようと思ったら

来月の今日
$raigetu = Carbon::addMonth()->toDateString();

みたいな感じでいい気がしますか?
では1月31日にこうやって日付取り出したらどうなりますか?
やってみてください。

うるう年かどうかによりますが、3月2日か3日が返ってきますね。
全然来月じゃない。
こんなロジックで請求書なんか出すシステム組んだ日にはえらいこっちゃですね。
通常、運用的にはこういう場合は翌月の最終日を返してほしいのです。

私は通常

来月の今日(2)
raigetu = date('Y-m-', strtotime('+1 month', strtotime(date('Y-m-01')))) . date('d');

みたいなややこしい書き方をしてました。Carbonのソースコード読むまでは。
このへんは人によっていろんなやり方があると思いますけど、月末で計算するから日数によって月またいだりいろいろしちゃうのであって、1日で計算したら問題ないでしょ?っていうやりかたです。
自分的には定型文ですが、他の人が見たら「なんじゃこりゃ」ってなりかねません。

でもCarbonのドキュメント読んでいてもそれっぽい機能が見当たらない。

で、仕方ないのでソースコード詠みこんでみたんですが、なんだかあっさりそれっぽいのが見つかりました。
その名も
addMonthNoOverflow

addMonthNoOverflow の使い方

って程の事はないです。

Carbon
// 今日が 1/31だと仮定して、翌月の今日を求めたい場合
$raigetu = Carbon::now()->addMonthNoOverflow()->toDateString();
# [結果]
# => "2019-02-28"

$raigetu = Carbon::parse("2019-03-31")->addMonthNoOverflow()->toDateString();
#[結果]
# => "2019-04-30"

// 1か月以上加算したいときは
//  Carbon::addMonthsNoOverflow(int)
// をつかいます。
$hantosi = Carbon::parse("2019-03-31")->addMonthsNoOverflow(6)->toDateString();
#[結果]
# => "2019-09-30"

あらまぁ・・・便利ですわね、奥さん!

ロジック的には、普通にaddMonthしてから、元の「日」と計算後の「日」が違ったら計算後の日付の「前月の最終日」を返す、っていう組み方みたいです。
速度的なロスを考えると、なんとか1行で書いてしまおうとしている私のやり方よりも賢いかもしれません。何より読みやすいし。

というわけで、ますます Carbon万歳 っていう流れになりましたね。

というところで、また次回!

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

Laravel で外部キー制約の onDelete / onUpdate をマイグレーション後に設定する + 論理削除でハマったこと

やりたいこと

  • CompanyEmployeeのように親子関係にあるテーブルを作成
  • employeeテーブルにcompany_idという外部キーを設定
  • その後、 onDelete onUpdateに対する挙動を設定し忘れたことに気づいたので後から設定をしたい

マイグレーションファイル

  • companies
timestamp_create_companies_table.php
class CreateCompanyTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('companies', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('company');
    }
}
  • employee
timestamp_create_employee_table.php
class CreateEmployeeTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('employee', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name')->nullable();
            $table->integer('company_id')->unsigned();
            $table->foreign('company_id')->references('id')->on('companies');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('employee');
    }
}

マイグレーションは問題なくて、その後も色々なテーブルを追加していった後、onDelete / onUpdate の設定ができていないことに気づいて、「全部ロールバックするのも気が重いし、一部分だけ弄れないかな…」と逡巡して以下の方法を取った。

やったこと

あまりお行儀がよくないかもしれないが、 employeeテーブルのcompany_idだけを drop するマイグレーションファイルを作り、php artisan migrateを実行。
その後外部キー制約を再設定するマイグレーションファイルを作って、同様にphp artisan migrate

  • 外部キーを一旦Drop
timestamp_drop_foreign_key_from_employee.php
class DropForeignKeyFromEmployee extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        //外部キー制約を引き剥がす時は dropForeign('テーブル名'_'外部キー名'_foreign);
        Schema::table('employee', function (Blueprint $table) {
            $table->dropForeign('employee_company_id_foreign');
        });
    }
}
timestamp_reset_foreign_key_to_employee.php
public function up()
    {
        //子テーブルに対象レコードがある場合、親テーブルのレコード削除を禁止 ->onDelete('restrict');
        //親テーブルのレコード更新は許可 ->onUpdate('cascade');
        Schema::table('employee', function (Blueprint $table) {
            $table->index('company_id');
            $table->foreign('company_id')->references('id')->on('companies')
                ->onDelete('restrict')
                ->onUpdate('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('employee', function (Blueprint $table) {
            $table->dropForeign('employee_company_id_foreign');
        });
    }

問題点

このやり方だとロールバックした時にDropForeignKeyFromEmployeeエラーで怒られる。なので、最初からなるべくテーブル作成時に onDelete / onUpdate の条件は慎重に考えて設定した方がいい。うまく後付けで設定できる方法があればコメントで教えていただきたいです。個人的に思いつくのがMySQLのコマンドで直接テーブルを書き換える手段だけなので…

その他ハマったこと

onDelete('restrict')を設定したあと、Laravel アプリケーションからCompany::destroyで実際に削除を試してみたところ、なんと制約に引っかからず普通に削除できてしまった…
どうやらuse SoftDeleteで論理削除をさせている場合、物理削除と違ってdeleted_atにタイムスタンプを書き込むだけなので制約をすり抜けてしまうらしい…。早めに知っておきたかった。

参考リンク
What if onDelete is restrict instead of cascade?

マイグレーションまとめ(Laravel5)

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

php-master-changes 2019-02-27

今日は .gdbinit の修正、ReflectionClass::getDefaultProperties() が型付きプロパティに使われた際の不具合の修正、二項演算子が PHP スクリプトのコンパイル時評価とランタイムで同じ結果を返すことのテストを追加する修正があった!

2019-02-27

nikic: Sync types in .gdbinit, improve property dumping

krakjoe: Fix #77673 ReflectionClass::getDefaultProperties returns spooky array

rjhdby: Add runtime_compile_time_binary_operands.phpt

bwoebi: Uncomment cleanup for runtime_compile_time_binary_operands.phpt

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

AWS LambdaのPHP関数のログ出力について

はじめに

前回の AWS Lambda のCustom RuntimeでPHP用のLayer作成 で作成したLayerを使用します。
(Stackery社のlayerでも問題ないです。)

作業環境

LayerのZip作成にdockerを、AWSへのアップロードにaws-cliを使います。

  • pyenv
    • 1.2.9-2
  • pip
    • 9.0.3(python3.6.5)
  • aws-cli
    • 1.16.96(python3.6.5)
  • sam
    • 0.14.2

作業はMacOSです。
各種インストールは割愛します。

使用ソースコード

https://github.com/Sunochi/LambdaTestFuntion

ログ出力について

AWS LambdaでPHPを使用した際の挙動として、ログ出力が不便という点があります。
API Gatewayを使用する場合はブラウザ上にvar_dump()で表示すれば良いですが、データとして残すためにもCloudwatch logsへ出来る限り可視性が高い状態で出力しておくことが望まれます。
最近はcloudwatch logsの機能で、json形式のログから検索も可能になっているようなので、ログ出力はちゃんとしようと思う毎日です。
さて、今回はprintfvar_dumperror_logの挙動の違い、現時点でのPHPでのログ出力方法のベストプラクティスをまとめておきます。

テストコード

index.php
<?php 
function print_log(){
    $temp_array = [
        'hoge' => "foo",
        'var'  => 2,
    ];
    printf("Hello World!\nhogehoge");
    printf($temp_array);
}

function var_dump_log(){
    $temp_array = [
        'hoge' => "foo",
        'var'  => 2,
    ];
    var_dump("Hello World!\nhogehoge");
    var_dump($temp_array);
}

function error_log_log(){
    $temp_array = [
        'hoge' => "foo",
        'var'  => 2,
    ];
    error_log("Hello World!\nhogehoge");
    $json_temp_array = json_encode($temp_array);
    error_log($json_temp_array);
}

function main(){
    print_log();
    var_dump_log();
    error_log_log();
}

結果

error_log()json_encodeの組み合わせが最適です。

printf

Lambda上のログ詳細: "Hello World!\nhogehogeArray"

CloudWatch logs: Hello World!
<行切り替え>hogehogeArray

printfで表示は可能ですが、複数のログを出力した際に、
1行にまとめて表示される問題があります。
\nがCloudwatch logsでの行切り替えになっているため、想定通りの出力にならないことがありそうです。

var_dump

Lambda上のログ詳細: "string(21) \"Hello World!\nhogehoge\"\narray(2) {\n  [\"hoge\"]=>\n  string(3) \"foo\"\n  [\"var\"]=>\n  int(2)\n}\n"

CloudWatch logs: string(21) "Hello World!
<行切り替え>hogehoge"
<行切り替え>array(2) {
<行切り替え>["hoge"]=>
<行切り替え>string(3) "foo"
<行切り替え>["var"]=>
<行切り替え>int(2)
<行切り替え>}

実際に出力してみるとわかりますが、とてもログを参考にできたものはないです。ブラウザ上に表示する場合はvar_dump()がいいと思いますが、ログ出力には向いていないのがわかると思います。

error_log

error_logにArrayを入れるとwarningが出るため、先にjson_encodeしておきます。()
```
Lambda上のログ詳細: ""

CloudWatch logs: Hello World!
<行切り替え>hogehoge
<行切り替え>
{
"hoge": "foo",
"var": 2
}
```

printf, var_dumpと比べて、行切り替えのタイミングが大分よくなりました。
ただ、\nは行切り替えしたくないので、以下のラッパー関数を作成します。

logging.php
function logging ($output) {
    $log = "";

    if(is_array($output)){
        $log = json_encode($output);
        $log = str_replace("\n", "\\n", $log);
    } else {
        $log = str_replace("\n", "\r", $log);
    }
    error_log($log);
}

結果

Lambda上のログ詳細: ""

CloudWatch logs: Hello World!
hogehoge
<行切り替え>
{
    "hoge": "foo",
    "var": 2
}

いいですね!Lambda上のログ詳細に出したい場合(デバッグ時)は、
var_dumpを使用すればいいと思います。

まとめ

PHPのログ出力について、ログ関数を入れてまで本格的にやりたくないので、手軽にできる方法を探しました。
今のところ、error_logのラッパー関数が使い心地がいいですが、よりオススメの方法があれば知りたいです。

参考

AWS Lambda Custom RuntimeでPHPからCloudWatch Logsに出力する方法

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

HomebrewでPHP7.2をインストール

環境

  • macOS Siera 10.12.6

インストール済みのPHPバージョン

$ php -v
PHP 5.6.30 (cli) (built: Oct 29 2017 20:30:32) 
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies

インストール

Formulaを探す。

$ brew search php72
==> Formulae
php@7.2
$ brew install php@7.2
==> Installing dependencies for php@7.2: apr, openssl, apr-util, argon2, aspell, autoconf, brotli, c-ares, libidn, libmetalink, libssh2, jansson, jemalloc, libev, libevent, nghttp2, openldap, rtmpdump, curl-openssl, libtool, unixodbc, freetds, libpng, freetype, gettext, libffi, pcre, glib, gmp, icu4c, jpeg, libpq, libsodium, libzip, readline, sqlite, tidy-html5 and webp
==> Installing php@7.2 dependency: apr
==> Downloading https://homebrew.bintray.com/bottles/apr-1.6.5.sierra.bottle.tar
######################################################################## 100.0%
==> Pouring apr-1.6.5.sierra.bottle.tar.gz
==> Caveats

確認

$ php -v
PHP 5.6.30 (cli) (built: Oct 29 2017 20:30:32) 
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies

ターミナルを再起動後、改めて確認するも変化なし。

シンボリックリンクの作成

$ brew link php72 --force
Linking /usr/local/Cellar/php@7.2/7.2.15... 25 symlinks created

If you need to have this software first in your PATH instead consider running:
  echo 'export PATH="/usr/local/opt/php@7.2/bin:$PATH"' >> ~/.bash_profile
  echo 'export PATH="/usr/local/opt/php@7.2/sbin:$PATH"' >> ~/.bash_profile

パスを通す

$ echo 'export PATH="/usr/local/opt/php@7.2/bin:$PATH"' >> ~/.bash_profile
$ echo 'export PATH="/usr/local/opt/php@7.2/sbin:$PATH"' >> ~/.bash_profile

再び確認

ターミナルを再起動後、確認。

$ php -v
PHP 7.2.15 (cli) (built: Feb 26 2019 10:47:19) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.15, Copyright (c) 1999-2018, by Zend Technologies

おしまい。

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