20190209のPHPに関する記事は12件です。

【PHP】バッチ処理をcronで自動実行する

バッチ処理とは

まとめて実施される処理のこと。

「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典〜バッチ処理

cronとは

cronとは、ジョブを自動実行するためのデーモンプロセス(バックグラウンドプロセス)のこと。

cronを設定するコマンドがcrontabコマンド

crontabコマンドについてまとめました 【Linuxコマンド集】

実際に動かす

ファイル構造

batch_test
  ├── add_time.php
  └── time.txt

add_time.php

現在時刻をtxtファイルに追記するだけのプログラムです。

cronで処理を実行する場合、ファイルパスは絶対パスで記述する必要があります。requireincludeを使用するときも同様です。

add_time.php
<?php
// time.txtに現在時刻を追記する
$file = '/usr/local/var/www/htdocs/batch_test/time.txt';
$current = file_get_contents($file);

date_default_timezone_set('Asia/Tokyo');
$current .= date("Y-m-d H:i:s")."\n";

file_put_contents($file, $current);

time.txt

追記用のファイルです。
こんな感じで現在時刻が追記されていきます。

time.txt
2019-02-09 22:20:00
2019-02-09 22:21:00
2019-02-09 22:22:00

ファイルを作成したら一度処理を実行してみます。
動作に問題がなければ、引き続きcronの設定に移ります。

$ cd batch_test
$ php add_time.php

cronの設定

cronの設定をcrontabファイルに記述します。

crontabファイルは「この時間(間隔)でこのファイルの処理を実行する」 という内容を記述するファイルです。crontab -eコマンドで編集できます。

以下の順番で記述します。

  1. 処理を実行する時間(または間隔)
  2. PHPのパス
  3. 処理が記述されているファイルのパス

プログラムの自動実行にcronを使おう サンプルプログラム付きで設定も簡単

最初にPHPの場所を確認します。パスは全て絶対パスで記述する必要があります。

$ which php
/usr/local/opt/php@7.1/bin/php

crontab -lコマンドでcronの設定を確認できます。
現在は何も設定されていません。

$ crontab -l
crontab: no crontab for name

crontab -eコマンドを実行するとviエディタが開くので、処理内容を記述します。(エディタは環境によって異なるかもしれません。)

add_time.phpの処理を1分おきに実行するように設定します。

*/1 * * * * /usr/local/opt/php@7.1/bin/php /usr/local/var/www/htdocs/batch_test/add_time.php

:wpで確かに保存しましたが、エラーが出てしまいました・・・

$ crontab -e
crontab: no crontab for name - using an empty one
crontab: "/usr/bin/vi" exited with status 1

$ crontab -e
crontab: no crontab for name - using an empty one
crontab: no changes made to crontab

これは:wqの前に:set backupcopy&viを実行することで解決できました。

crontab でno changes made to crontabで変更できない時

これで登録されました!

$ crontab -e
crontab: no crontab for name - using an empty one
crontab: installing new crontab

$ crontab -l
*/1 * * * * /usr/local/opt/php@7.1/bin/php /usr/local/var/www/htdocs/batch_test/add_time.php

もっと複雑な処理をさせたいのですが、今日はこの辺で。

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

php-master-changes 2019-02-08

この日はドキュメントの修正、ZEND_OVERLOADED_FUNCTION の削除、エンジンのコードの単純化、curl バージョンアップに伴うテスト期待出力の修正、FTPS を passive で使う際に見るチャンネルを間違っていた問題の修正、SplHeap::compare() の arginfo でのシグネチャ修正、fsockopen() のメモリ解放処理の修正、内部向け API として ZEND_TRY_ASSIGN_BOOL() の追加、trait 由来のプロパティの型名について参照カウントの扱いが誤っていた問題の修正、型付プロパティの後置インクリメントが例外を投げる際にメモリの二重解放が起きてしまう問題の修正、openssl_encrypt()、openssl_decrypt() の処理内で使う出力用バッファの解放処理が誤っていた問題の修正があった!

2019-02-08

petk: [ci skip] Update changelog

dstogov: Remove ZEND_OVERLOADED_FUNCTION and corresponding call_method object handler

dstogov: Simplify checks

petk: [ci skip] Move OPcache configure option changes

weltling: Sync test for libcurl 7.64.0

weltling: Sync test with changes in libcurl 7.64.0

tsoftware-org: Fix FTPS passive mode of data channel event poll

morrisonlevi: Fix SplHeap::compare arginfo and tests

nikic: Fix invalid free

nikic: Add ZEND_TRY_ASSIGN_BOOL API

nikic: Fix refcounting of prop types coming from traits

nikic: Fix double free if post inc of typed property throws

nikic: Fix incorrect outbuf freeing

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

PHP/ヒアドキュメントについて

ヒアドキュメント

長文を出力したいとき、一文ずつにechoをかけながら\nで改行していくのは面倒すぎます。
ヒアドキュメント使ってみましょう。

例文として聖書から拝借します。

The LORD bless thee, and keep thee:
The LORD make his face shine upon thee, and be gracious unto thee:
The LORD lift up his countenance upon thee, and give thee peace.

> Numbers 6:24-26 King James Version (KJV) / Bible

使い方

<?php

echo <<< EOM

The LORD bless thee, and keep thee:
The LORD make his face shine upon thee, and be gracious unto thee:
The LORD lift up his countenance upon thee, and give thee peace.

EOM;

?>

これだけです。

出力は

The LORD bless thee, and keep thee:
The LORD make his face shine upon thee, and be gracious unto thee:
The LORD lift up his countenance upon thee, and give thee peace.

うまくいってますね:v:

開始と終了

上ではEOMと書きましたが、本当はなんでもいいみたいです。
よく使われるのはEOM(End Of Message),EOD(End Of Document),EOF(End Of File)とか。
ABCとかでもいけます。要はただの合言葉です。

ただ、他の変数とか関数と見分けがつきやすいように、全部大文字にした方がいいと思います:relaxed:

変数展開

では、変数展開の話をしていきます。
他の記事でも詳しく触れようと思ってるんですが、せっかくなので。

PHPにおいて変数展開できるのは""(ダブルクオーテーション)とヒアドキュメントです。
''(シングルクオーテーション)だと変数展開できません。

少し実験してみましょう!
先ほどの聖書の文章で、The LORDという部分を変数に入れてみます。

まずはヒアドキュメントから。

<?php
$ld = "The LORD";

echo <<< EOD

$ld bless thee, and keep thee:
$ld make his face shine upon thee, and be gracious unto thee:
$ld lift up his countenance upon thee, and give thee peace.

EOD;

?>

出力は

The LORD bless thee, and keep thee:
The LORD make his face shine upon thee, and be gracious unto thee:
The LORD lift up his countenance upon thee, and give thee peace.

うまくいってます。

次、ダブルクオーテーション。

<?php
$ld = "The LORD";
echo "$ld bless thee, and keep thee:";
?>

出力は

The LORD bless thee, and keep thee:

面倒だったので一文でやりましたが、うまくいきましたね。

最後にシングルクオーテーション。

<?php
$ld = "The LORD";
echo '$ld bless thee, and keep thee:';
?>

出力は

$ld bless thee, and keep thee:

変数がそのまま出てきました。

シングルクオーテーションでは変数が展開されないのがわかって頂けたと思います:eyes:
駆け足+本筋から外れましたが、以上!!

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

AmazonLinux2のMariaDBをPHPで操作する

ファイルの作成

AmazonLinux2にrootユーザーでログインします。

AmazonLinux2
login as: ec2-user

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[ec2-user ~]$ sudo -i
[root ~]#

ドキュメントルートに移動します。

AmazonLinux2
[root ~]# cd /var/www/html
[root html]#

フォルダを作成して、そこに移動します。

AmazonLinux2
[root html]# mkdir php
[root html]# cd php
[root php]#

phpファイルを作成します。

AmazonLinux2
[root php]# touch test.php

ファイルの編集

phpファイルを編集します。

AmazonLinux2
[root php]# vim test.php

これが編集画面です。

AmazonLinux2
~
~
~
"test.php" [New File]                                         0,0-1         All

[A]キーを押してINSERTモードに移行します。
このモード中だけファイルの中身を編集できます。

AmazonLinux2
~
~
~
-- INSERT --                                                  0,1           All

[ESC]キーでINSERTモードを終了します。

AmazonLinux2
~
~
~
"test.php" [New File]                                         0,0-1         All

[*][w][q]と入力して[Enter]で上書き保存します。

AmazonLinux2
~
~
~
:wq

編集画面:その他の操作方法

保存しないで終了

AmazonLinux2
:q

内容を全て削除

AmazonLinux2
:%d

SELECT文

Select.php
<?php
        //データベース情報
        $user = 'root';
        $pass = '';
        $dsn = 'mysql:dbname=test; host=127.0.0.1; port=3306; charset=utf8';
        //URLからパラメータを取得
        $id = $_GET["id"];

        try {
                //データベースに接続
                $pdo = new PDO($dsn, $user, $pass);
                //SQL文を用意
                $sql = "SELECT * FROM table1 WHERE id = ?";
                //SQL文をpStmtクラス変数に入れる
                $pStmt = $pdo->prepare($sql);
                //SQLを実行
                $pStmt->execute(array($id));
                //戻り値を取りだす
                foreach( $pStmt as $value )
                {
                        echo $value[id];
                        echo $value[name];
                }
        } catch(PDOException $e) {
                echo $e->getMessage();
                die();
        }
?>

UPDATE文

Update.php
<?php
        //データベース情報
        $user = 'root';
        $pass = '';
        $dsn = 'mysql:dbname=test; host=127.0.0.1; port=3306; charset=utf8';
        //URLからパラメータを取得
        $id = $_GET["id"];
        $name = $_GET["name"];

        try {
                //データベースに接続
                $pdo = new PDO($dsn, $user, $pass);
                //SQL文を用意
                $sql = "UPDATE table1 SET name = ? WHERE id = ?";
                //SQL文をpStmtクラス変数に入れる
                $pStmt = $pdo->prepare($sql);
                //SQLを実行
                $pStmt->execute(array($name,$id));
        } catch(PDOException $e) {
                echo $e->getMessage();
                die();
        }
?>

INSERT文

Insert.php
<?php
        //データベース情報
        $user = 'root';
        $pass = '';
        $dsn = 'mysql:dbname=test; host=127.0.0.1; port=3306; charset=utf8';
        //URLからパラメータを取得
        $id = $_GET["id"];
        $name = $_GET["name"];

        try {
                //データベースに接続
                $pdo = new PDO($dsn, $user, $pass);
                //SQL文を用意
                $sql = "INSERT INTO table1 (id, name) VALUES (?,?)";
                //SQL文をpStmtクラス変数に入れる
                $pStmt = $pdo->prepare($sql);
                //SQLを実行
                $pStmt->execute(array($id,$name));
        } catch(PDOException $e) {
                echo $e->getMessage();
                die();
        }
?>

DELETE文

Delete.php
<?php
        //データベース情報
        $user = 'root';
        $pass = '';
        $dsn = 'mysql:dbname=test; host=127.0.0.1; port=3306; charset=utf8';
        //URLからパラメータを取得
        $id = $_GET["id"];

        try {
                //データベースに接続
                $pdo = new PDO($dsn, $user, $pass);
                //SQL文を用意
                $sql = "DELETE FROM table1 WHERE id = ?";
                //SQL文をpStmtクラス変数に入れる
                $pStmt = $pdo->prepare($sql);
                //SQLを実行
                $pStmt->execute(array($id));
        } catch(PDOException $e) {
                echo $e->getMessage();
                die();
        }
?>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHPでデザインパターン】Singleton

デザインパターンの学習の一環で、各デザインパターンの実装をPHPで行ってみます。
また、振る舞いの確認としてPHPUnitを使用しています。

デザインパターンとは

ソフトウェア開発におけるデザインパターン(型紙(かたがみ)または設計パターン、英: design pattern)とは、過去のソフトウェア設計者が発見し編み出した設計ノウハウを蓄積し、名前をつけ、再利用しやすいように特定の規約に従ってカタログ化したものである。

引用元:デザインパターン (ソフトウェア) – Wikipedia( https://ja.wikipedia.org/wiki/デザインパターン_(ソフトウェア)

つまり過去のエンジニアが解決してきた方法が、パターンとしてまとめられているのがデザインパターンです。

デザインパターンと言ってもいろいろとありますが、最も有名なのはGoF(Gnag of Four)のまとめた23パターンかと思います。

今回はその23パターンのうちの一つである、Singletonパターンについて実装していきます。

Singletonパターンとは

Singleton パターンとは、そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのことである。ロケールやルック・アンド・フィールなど、絶対にアプリケーション全体で統一しなければならない仕組みの実装に使用される。

引用元:Singleton パターン – Wikipedia( http://ja.wikipedia.org/wiki/Singleton_パターン

クラスのインスタンスは通常new演算子を使って生成されます。
これが1000回実行されるとすると、1000個のインスタンスが生成されることになり、当然コストのかかる処理になってきます。

そこで、1度生成されたインスタンスを使いまわしたいという場面が出てきます。
また、システム上どうしても1つしかインスタンスを生成したくない場面もあるかと思います。

いずれの場合においても、new演算子を1度しか使わず、そこで生成されたインスタンスを使い回せばいいものですが、当然ミスは起こりえます。

開発者が意識せずとも、あるクラスのインスタンスが1つのみであることを保証するために使われるデザインパターンが、Singletonパターンです。

コード

では、どうしたらインスタンスを1つしか作らないクラスを作れるか、という話になりますが、PHPでは簡単に実装できます。

Singleton.php
<?php
...

class Singleton
{
    private static $singleton;

    // new Singleton()でインスタンスを作成できないよう、アクセス修飾子はprivateにする
    private function __construct()
    {
    }

    // このstaticメソッドで、Singletonクラスのインスタンスを作成する
    public static function getInstance(): Singleton
    {
        if (self::$singleton == null) {
            self::$singleton = new Singleton();
        }

        return self::$singleton;
    }

    // インスタンスのクローンを許可しないようにする
    final function __clone()
    {
        throw new \Exception(sprintf('Clone is not allowed: %s', get_class($this)));
    }
}

動作確認

動作確認用に、以下のテストコードを書いて実行してみます。
確認内容は以下の3つに絞っています。

  • new Singletonでインスタンス化できないようになっているか
  • getInstanceメソッドは、同一のインスタンスを返すか
  • getInstanceで作成したインスタンスをcloneすることによる、別インスタンスの作成ができないか
SingletonTest.php
<?php
...

class SingletonTest extends TestCase
{
    public function testConstructIsNotPublic()
    {
        $reflection = new \ReflectionClass('App\Singleton\Singleton');
        $constructor = $reflection->getConstructor();
        $this->assertFalse($constructor->isPublic());
    }

    public function testGetInstanceReturnsSameInstance()
    {
        $instance_1 = Singleton::getInstance();
        $instance_2 = Singleton::getInstance();

        $this->assertTrue($instance_1 === $instance_2);
    }

    public function testCloneThrowsException()
    {
        $instance_1 = Singleton::getInstance();

        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Clone is not allowed: App\Singleton\Singleton');
        $instance_2 = clone $instance_1;
    }
}

実行してみます。

# phpunitを実行
./vendor/bin/phpunit tests --colors=always

# 結果
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 36 ms, Memory: 4.00MB

OK (3 tests, 4 assertions)

無事テストが通りました!

Singletonパターンに関しては、以上となります。
使いどころは考える必要がありますが、PHPで簡単に実装することができました。

■ コード
https://github.com/masato-d/design-pattern

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

PHPでデザインパターン【Singleton】

デザインパターンの学習の一環で、各デザインパターンの実装をPHPで行ってみます。
また、振る舞いの確認としてPHPUnitを使用しています。

デザインパターンとは

ソフトウェア開発におけるデザインパターン(型紙(かたがみ)または設計パターン、英: design pattern)とは、過去のソフトウェア設計者が発見し編み出した設計ノウハウを蓄積し、名前をつけ、再利用しやすいように特定の規約に従ってカタログ化したものである。

引用元:デザインパターン (ソフトウェア) – Wikipedia( https://ja.wikipedia.org/wiki/デザインパターン_(ソフトウェア)

つまり過去のエンジニアが解決してきた方法が、パターンとしてまとめられているのがデザインパターンです。

デザインパターンと言ってもいろいろとありますが、最も有名なのはGoF(Gnag of Four)のまとめた23パターンかと思います。

今回はその23パターンのうちの一つである、Singletonパターンについて実装していきます。

Singletonパターンとは

Singleton パターンとは、そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのことである。ロケールやルック・アンド・フィールなど、絶対にアプリケーション全体で統一しなければならない仕組みの実装に使用される。

引用元:Singleton パターン – Wikipedia( http://ja.wikipedia.org/wiki/Singleton_パターン

クラスのインスタンスは通常new演算子を使って生成されます。
これが1000回実行されるとすると、1000個のインスタンスが生成されることになり、当然コストのかかる処理になってきます。

そこで、1度生成されたインスタンスを使いまわしたいという場面が出てきます。
また、システム上どうしても1つしかインスタンスを生成したくない場面もあるかと思います。

いずれの場合においても、new演算子を1度しか使わず、そこで生成されたインスタンスを使い回せばいいものですが、当然ミスは起こりえます。

開発者が意識せずとも、あるクラスのインスタンスが1つのみであることを保証するために使われるデザインパターンが、Singletonパターンです。

コード

では、どうしたらインスタンスを1つしか作らないクラスを作れるか、という話になりますが、PHPでは簡単に実装できます。

Singleton.php
<?php
...

class Singleton
{
    private static $singleton;

    // new Singleton()でインスタンスを作成できないよう、アクセス修飾子はprivateにする
    private function __construct()
    {
    }

    // このstaticメソッドで、Singletonクラスのインスタンスを作成する
    public static function getInstance(): Singleton
    {
        if (self::$singleton == null) {
            self::$singleton = new Singleton();
        }

        return self::$singleton;
    }

    // インスタンスのクローンを許可しないようにする
    final function __clone()
    {
        throw new \Exception(sprintf('Clone is not allowed: %s', get_class($this)));
    }
}

動作確認

動作確認用に、以下のテストコードを書いて実行してみます。
確認内容は以下の3つに絞っています。

  • new Singletonでインスタンス化できないようになっているか
  • getInstanceメソッドは、同一のインスタンスを返すか
  • getInstanceで作成したインスタンスをcloneすることによる、別インスタンスの作成ができないか
SingletonTest.php
<?php
...

class SingletonTest extends TestCase
{
    public function testConstructIsNotPublic()
    {
        $reflection = new \ReflectionClass('App\Singleton\Singleton');
        $constructor = $reflection->getConstructor();
        $this->assertFalse($constructor->isPublic());
    }

    public function testGetInstanceReturnsSameInstance()
    {
        $instance_1 = Singleton::getInstance();
        $instance_2 = Singleton::getInstance();

        $this->assertTrue($instance_1 === $instance_2);
    }

    public function testCloneThrowsException()
    {
        $instance_1 = Singleton::getInstance();

        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Clone is not allowed: App\Singleton\Singleton');
        $instance_2 = clone $instance_1;
    }
}

実行してみます。

# phpunitを実行
./vendor/bin/phpunit tests/singleton --colors=always

# 結果
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 36 ms, Memory: 4.00MB

OK (3 tests, 4 assertions)

無事テストが通りました!

Singletonパターンに関しては、以上となります。
使いどころは考える必要がありますが、PHPで簡単に実装することができました。

■ コード
https://github.com/masato-d/design-pattern

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

アプリにチャレンジ2日目

今日はHerokuでアプリ、2日目です。

前回は こちらの Web を参考に作ってみたのですが、何をやっているかさっぱりわかりませんでしたので、今日はコマンド不要で超簡単バージョンにチャレンジしてみました。

2018-05-09
コマンド不要で超簡単!HerokuでWebアプリ開発を30分で始める【php+postgres】
http://tech-blog.rakus.co.jp/entry/2018/05/09/100346

こちら、大変分かりやすいガイドでした!
Heroku、PHP、Postgresと、Webアプリは最初の準備がめちゃくちゃハードルが高くて、あきらめる人も多いのですが、これなら始めやすくていいなーと思いました。

1.ヘロクにログインします。これは昨日作ったアカウントでOK.

2.ギットハブにログインします。
これは新たにアカウントを作りました。

3.ヘロクに PHP と Postgres のアプリを追加。
昨日あーだこーだやったので既に5個のアプリができていて、これ以上は有料というので消しました。ちなみにこの辺の解説、全部英語で出てくるのでGoogle 翻訳でコピペしながら進めました。マジで英語やんなきゃやべえなおいって感じです。

4.セッティングからアッドビルドパックを選んで「PHP」を追加。無料プランでやりますと宣言、二日目のアプリなので「Day2」という名前をつけて登録しました。

5.パソコンのデスクトップに「Day2」というフォルダを作って、その中に「index.php」というファイルを作って、簡単な文字とPHPの動作確認画面を表示するように書いておきます。

6.ギットハブデスクトップをダウンロードしてインストール。ギットハブのアカウントとつなげて、デスクトップの「Day2」フォルダを作って、アップロードします。

7.もう1回ヘロクに戻って、ギットハブを選んで「 connect to github 」を選んで、「day2」と入力してポチッとな。 ギットハブとヘロクを連携するか聞かれるのでもちろんYES。これで繋がりましたと。

8.オートマティックデプロイズにあるイネーブルオートマティックデプロイズを選んでポチッとな。

9.デプロイ1回目はマニュアルでデプロイしなきゃなので、マニュアルデプロイを選んで デプロイブランチをポチッと。そうするとギットハブにアップしたファイルを読み込んで、PHPを動かしますよーというメッセージがざざざーっとながれてきて、、、あら失敗。

 !     ERROR: Application not supported by this buildpack!
<中略>
 !     Push failed

ギットハブに戻って確認すると、index.phpがアップロードされてなかったので、もう1回 アップロードを選んで、念のため ギットハブのブラウザ版にアクセスしてindex.phpがあるって事を確認して、もっかいヘロクからマニュアルデプロイ、ポチッとな。 今度はできましたできましたーとメッセージがととととって出てきました。

10.下の方にビューというボタンが出てきました。こいつをポチり。そうするとindex.phpに書いておいたテスト用の文字と、動作確認画面が表示されました!バンザイ。

https://appday2.herokuapp.com/

(所要時間60分)

今日も、なんだかよく分かりませんでしたが、まあやってるうちに分かるでしょう。 ではまた明日ーバイバーイ!

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

【Windows】Scoop+VSCode+PHPでローカルデバッグ環境を構築する

PHPのWebアプリ(Apache, PHP7.2, MySQL, Composer)をWindows10のローカル上でデバッグするための環境構築手順です。

既に同じような記事がありますが、Scoopを使ったものがなかったので、備忘の用途も含めて投稿してみました。

※今回、記事で使用しているPowerShellの関数をGistにまとめているので、逐一関数を宣言するのが面倒な場合はご使用ください。

この記事でできること

  • Visual Studio Codeを使って補完入力や使用箇所の参照・移動など
  • Apache, PHP(記事を書いている段階ではPHP7.2), MySQLでWebアプリの実行
  • MySQL Workbenchを使ったデータベース操作
  • Webアプリや単一スクリプトのデバッグ
  • Composerを使ったライブラリの管理
  • ScoopによるPHPのバージョン更新・切替

環境構築する上での方針

  1. 環境構築手順をコード(or ps1スクリプト)で管理しやすいような方法を用いる。
    → PowerShellやScoop(パッケージ管理ツール)などを使ってインストールしていきます。1
  2. ネカフェとかのPCでも構築できる。
    → 管理者権限やPC再起動、コントロールパネル操作を不要にしています。
  3. 全て無料のソフトを使用する。
    → PHPStormだと有料になるので、Visual Studio Codeを使うことにしました。

Scoopで各アプリをインストール

以下、全てPowerShellコンソール上で実行してください。
※ PowerShellのv3以上が必要ですが、Windows7の初期状態だとv2のためアップグレードが必要となります。
  ここでは割愛しますが、ググるといろいろ出てきます。

PowerShell
# Scoop自身をインストール(Scoop内部でGitも使用しているため、そちらもインストールが必要)
Set-ExecutionPolicy RemoteSigned -scope CurrentUser
iex (new-object net.webclient).downloadstring('https://get.scoop.sh')
scoop install git

# bucket(パッケージリポジトリ的な奴)を追加
scoop bucket add extras
scoop bucket add versions

# Visual Studio Codeのインストール
scoop install vscode

# 各アプリをインストール
# phpとxdebugでバージョンが異なる可能性があるため、バージョン指定のScoop Appをインストールする。
# mysql-workbenchはGUIユーザ向けなので不要な人は割愛してください。
scoop install apache php72 php72-xdebug composer mysql-workbench mysql

## -- 6つのアプリを一気にインストールするので、ちょっと時間がかかります。--

# PHPを実行するのに必要なコンポーネントをインストール。
# 一度インストール完了すれば、Scoop上からは不要なので削除する。
scoop install vcredist2017; scoop uninstall vcredist2017

# インストールしたアプリの確認
# 7zipはScoop内部で使用しているので、勝手にインストールされます。
scoop list
#=> 7zip 18.06
#=> apache 2.4.38
#=> composer 1.8.3
#=> git 2.20.1.windows.1
#=> mysql 8.0.15
#=> mysql-workbench 8.0.15
#=> php72 7.2.14 [versions]
#=> php72-xdebug 2.6.1-7.2 [versions]
#=> vscode 1.31.0 [extras]

補足1:PHPをScoopでインストールするときはバージョン指定のものを推奨

Scoopからインストール可能なPHPにはいくつかのエディションがあり、検索すると以下のようになっています。

PowerShell
scoop search php
'main' bucket:
    php-nts (7.3.1)
    php (7.3.1)

'extras' bucket:
    appengine-go (1.9.70) --> includes 'php_cli.ps1'
    eclipse-php (2018-12)
    php-nts-xdebug (2.6.1-7.2)
    php-xdebug (2.6.1-7.2)

'versions' bucket:
    php54 (5.4.45)
    php55-xdebug (2.5.5-5.5)
    php55 (5.5.38)
    php56-xdebug (2.5.5-5.6)
    php56 (5.6.40)
    php70-xdebug (2.6.1-7.0)
    php70 (7.0.33)
    php71-xdebug (2.6.1-7.1)
    php71 (7.1.26)
    php72-xdebug (2.6.1-7.2)
    php72 (7.2.14)
    php73 (7.3.1)

ここでphpとphp-xdebugに注目してほしいのですが、
phpのバージョンが7.3.1になっているのに対し、php-xdebugの方は2.6.1-7.2とphp7.2用のXdebugしか利用できない状況となっています。
記事を書いているときにたまたまバージョンが違っているだけですが、この2つは常に同じバージョンになることが保証されていないため、
インストールする際は基本的にversions bucket内で提供されているPHPバージョン付きのAppを使用した方がトラブル回避できると思います。

なお、versions bucketだと現時点でノンスレッドセーフ版(NTS)が登録されていないため、
ノンスレッドセーフ版を使いたい場合は、PHP bucketからインストールすることが可能です。
こちらを使用したい場合は、scoop bucket add phpとすることでローカルのScoopからinstallsearchが実行できるようになります。
今回はこちらは使用せず、phpもphp-xdebugもversions bucketのものを使用していきます。

補足2:Composerのグローバル環境

通常、Composerでglobal環境にライブラリをダウンロードした場合、
%APPDATA%/Composer配下にcomposer.jsonvendorなどが作成されますが、
Scoop経由でインストールしたComposerの場合、~\scoop\persist\composer\home\配下に%APPDATA%/Composer同様のリソースが作成されます。

なお、~\scoop\persist配下のファイルはアプリのバージョンが更新(scoop update composerなど)されても保持される設定ファイル等が配置されています。
Composerの場合、global環境にインストールしたことにより更新されたcomposer.jsonvendorディレクトリはComposer自体が更新された後も保持され続けます。
逆に言えば、それ以外のデータはバージョン間での引継ぎはありません。

補足3:カスペルスキーのデフォルト設定だとComposerでrequireできない

ComposerのIssuesで既に報告されていますが、
カスペルスキー使用中のPCだと初期設定ではComposerが利用できないみたいです。実際に私も遭遇しました。

エラー内容
composer require phpunit/phpunit
#=> The "https://repo.packagist.org/packages.json" file could not be downloaded: SSL operation failed with code 1. #=> #=> OpenSSL Error messages:
#=> error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed
#=> Failed to enable crypto
#=> failed to open stream: operation failed
#=> https://repo.packagist.org could not be fully loaded, package information was loaded from the local cache and may be out of date
#=> ...

解決方法としては、設定->詳細->ネットワーク->暗号化された接続のスキャンで「暗号化された接続をスキャンしない」に変更すれば解決しました。2

Apache関連の設定

Scoop経由でインストールしたApacheですと、httpd.conf~\scoop\persist\apache\conf\httpd.confに作成されるため、
以降こちらのファイルに対して必要な設定を入れていきます。
なお、こちらのファイルは既述の通りApache自体を更新してもバージョン間で共有されるファイルとなります。

LoadModule、AddHandler、PHPIniDirの設定

ScoopのWikiで記載されている通りのやり方で、Scoop経由でインストールしたPHPの情報をhttp.confに追記します。

iex (new-object net.webclient).downloadstring('https://gist.githubusercontent.com/nilkesede/c98a275b80b6d373131df82eaba96c63/raw/apache-php-init.ps1')

上記のコマンド3によってPHPの設定情報が反映できるようになっています。
以下、反映される内容です。

~\scoop\persist\apache\conf\httpd.conf
+  # php setup
+   LoadModule php7_module 'C:/Users/xxxxx/scoop/apps/php72/current/php7apache2_4.dll'
+   AddHandler application/x-httpd-php .php
+   PHPIniDir "C:\Users\xxxxx\scoop\apps\php72\current"

DocumentRootの変更

Apacheのhttpd.confでDocumentRootを ~\scoop\persist\apache\htdocs から同階層下に作成する予定のpublicディレクトリに変更します。
対した処理ではないので、普通に手で編集してもいいと思いますが、
自動化したいニーズが発生した時にすぐ自動化できるよう、あえてPowerShellで書き換えます。

# BOMなしのUTF-8で書き込むため、割と複雑になってます。
function replaceFileContentWithoutBOM($path, $regex, $replacement) {
  Get-Content -Encoding UTF8 $path `
    | % {$_ -replace $regex, $replacement}`
    | Out-String `
    | % { [Text.Encoding]::UTF8.GetBytes($_) } `
    | Set-Content -Path $path -Encoding Byte
}

replaceFileContentWithoutBOM ~\scoop\persist\apache\conf\httpd.conf '\${SRVROOT}/htdocs' '${SRVROOT}/htdocs/public'

反映される内容は以下の通りです。

~\scoop\persist\apache\conf\httpd.conf
-DocumentRoot "${SRVROOT}/htdocs"
-<Directory "${SRVROOT}/htdocs">
+DocumentRoot "${SRVROOT}/htdocs/public"
+<Directory "${SRVROOT}/htdocs/public">

php.iniに設定事項を反映

Scoopでインストールしたphp.iniに設定を適用したい場合は、
~\scoop\persist\php72\cli\conf.d配下に${任意のファイル名}.iniを作成し、
その中に追加分の設定値を記入していきます。

基本的な設定

新しくcustom.iniというファイルを作成し、そのファイルに設定値を記入します。
以下はphp.iniの最小限の設定例として記載してます。

~\scoop\persist\php72\cli\conf.d\custom.ini
extension=curl
extension=mbstring
extension=openssl
extension=pdo_mysql
extension=pdo_sqlite

[Date]
date.timezone = Asia/Tokyo

[mbstring]
mbstring.language = Japanese
mbstring.encoding_translation = Off
mbstring.detect_order = auto
mbstring.substitute_character = none

Xdebugに関する設定

Xdebugに関する設定値はxdebug.iniというファイルにまとめていきます。
既にいくつかの設定値が記入されているので、その後に設定値を追記します。

~\scoop\persist\php\cli\conf.d\xdebug.ini
zend_extension=C:\Users\xxxxx\scoop\apps\php72-xdebug\current\php_xdebug.dll
[xdebug]
xdebug.remote_enable=on
xdebug.remote_autostart=on
;xdebug.remote_connect_back=on
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000

xdebug.remote_connect_backがデフォルトだとonですが、それだとxdebug.remote_hostの値が無視されるためコメントアウトにしています。
Xdebugの設定に関する情報は [PHP] Xdebug のリモートデバッグ、理解していますか? - Qiita が参考になりました。

ユーザ環境変数「PATH」にPHPIniDirとComposerのbinを追加

Scoopのデフォルト設定だと、特別な方法でphp.exephp-cgi.exephpdbg.exeのみにPATHを通していますが、
Apacheと連携させるためには、php自体のディレクトリにPATHを通す必要があります。
また、ComposerでglobalにインストールしたライブラリにもPATHが通されるよう、合わせて設定を追加していきます。

今回は冒頭で書いた「環境構築する上での方針」に従うため、コンソール上でそのまま追加することにします。
(PowerShellの再起動とかは不要です)

PowerShell
function env($name,$global,$val='__get') {
    $target = 'User'; if($global) {$target = 'Machine'}
    if($val -eq '__get') { [environment]::getEnvironmentVariable($name,$target) }
    else { [environment]::setEnvironmentVariable($name,$val,$target) }
}

function ensure_in_path($dir, $global) {
    $path = env 'PATH' $global
    $dir = (Resolve-Path $dir).Path
    if($path -notmatch [regex]::escape($dir)) {
        env 'PATH' $global "$dir;$path" # for future sessions...
        $env:PATH = "$dir;$env:PATH" # for this session
    }
}

ensure_in_path ~\scoop\apps\php72\current
ensure_in_path ~\scoop\persist\composer\home\vendor\bin

PowerShellでPATHを通す作業はいろいろ辛いです。
上記のコードはScoop内部で使用している関数をそのまま拝借しました。

VSCodeでPHP開発に便利な拡張アプリをインストール

PHPの拡張機能で人気どころをインストールします。

PoserShell
# 補完、ドキュメント参照、リファクタリング、コードナビゲーション、など
code --install-extension felixfbecker.php-intellisense
# デバッグ機能(ブレークポイント、ステップ(オーバー|イン|アウト)、変数ウォッチ)
code --install-extension felixfbecker.php-debug

※ PHP Intellisenseを使う場合、以前はlanguage-serverを別途インストールする必要がありましたが、
  現在はこちらの拡張機能内に同梱されているので、別途作業は不要となります。

プロジェクト固有の設定

ここからはプロジェクト固有の設定について説明していきます。

まず、任意の場所にsample_projectディレクトリを作成し、
プロジェクト固有の設定を追加していきます。

PowerShell
New-Item -ItemType Directory -Name sample_project -Force
cd sample_project

# Visual Studio Code用に各設定ファイルの保存先を作成
New-Item -ItemType Directory -Name .vscode -Force
# ApacheのDocumentRootとなるディレクトリ
New-Item -ItemType Directory -Name public -Force

# Visual Studio Codeでプロジェクトフォルダを開く
code .

PHP関連の設定

プロジェクト内で使用するPHP関連の設定です。

Composer: 依存管理ツール

PHPプロジェクトで使用するライブラリを管理するために既にScoopでインストールしているComposerを使っていきます。

まず、sample_projectディレクトリ直下に以下のcomposer.jsonを作成します。

sample_project\composer.json
{
  "name": "nimzo6689/sample_project",
  "description": "Sample Project - sample application for demo purpose.",
  "type": "project",
  "license": "Unlicense",
  "authors": [
    {
      "name": "Taro Nimzo",
      "email": "nimzo6689@example.com"
    }
  ],
  "require": {
    "twig/twig": "^2.0"
  },
  "require-dev": {}
}

次にターミナルでcomposer installを実行すれば、Composerがcomposer.jsonの設定値を元に
require、または、require-devに指定したライブラリをvendorディレクトリ配下にダウンロードし、
実際にインストールした内容をcomposer.lockに書き込んでくれます。

PowerShell(pwd=sample_project)
# 依存しているライブラリをインストール
composer install
#=> Loading composer repositories with package information
#=> Updating dependencies (including require-dev)
#=> Package operations: 3 installs, 0 updates, 0 removals
#=>   - Installing symfony/polyfill-ctype (v1.10.0): Loading from cache
#=>   - Installing symfony/polyfill-mbstring (v1.10.0): Loading from cache
#=>   - Installing twig/twig (v2.6.2): Downloading (100%)
#=> Writing lock file
#=> Generating autoload files

Composerのより詳細に関してはcomposer --helpを見ればコマンドの使い方については大体わかるようになっています。
Composer自体の基礎的なところに関しては PHP開発でComposerを使わないなんてありえない!基礎編 - Qiita がわかりやすいですし、
公式ドキュメント もよく整備されていて読みやすいです。

Visual Studio Code

Visual Studio Codeではプロジェクト(正確にはワークスペース)単位でいくつかのjsonファイルによってチーム間で設定を共有することができます。
よって、以降PHPに関連する設定の仕方を定義していきます。

settings.json: エディタの基本的な設定

チーム全体でプロジェクトの設定が共有できるように、.vscode\settings.json内にプロジェクト固有の設定を反映します。
ここでいう設定には、Visual Studio Code自体の設定と拡張機能の設定、両方が入ります。

sample_project\.vscode\settings.json
{
  "php.validate.executablePath": "C:\\Users\\xxxxx\\scoop\\apps\\php72\\current\\php.exe",
  "php.executablePath": "C:\\Users\\xxxxx\\scoop\\apps\\php72\\current\\php.exe",
  "php.suggest.basic": false,
  "files.watcherExclude": {
    "**/.git/objects/**": true,
    "**/.git/subtree-cache/**": true,
    "**/node_modules/**": true,
    "**/vendor/**": true
  }
}

上記のsettings.jsonを作成すると、Visual Studio Codeの再起動が求められるので、指示通り再起動します。

解説1:PHP Intellisense関連の設定

上記で記載したjsonファイルをそのままコピペすると実行可能なPHPが見つかりません、というエラーが出ると思います。

image.png

PHP IntellisenseではInstallationの説明文にある通り、ローカルにインストールしたphpのパス(php.*.executablePath)を指定する必要があるので、こちらは各PC環境に応じて修整してください。

ちなみにですが、\がエスケープ済みのphpのパスを取得する場合、以下のコマンドで取得できます。

PowerShell
(scoop which php) -replace '\\', '\\'
#=> C:\\Users\\xxxxx\\scoop\\apps\\php\\current\\php.exe

また、PHP IntellisenseではVisual Studio Codeが持っている機能と競合をさけるためにphp.suggest.basicを無効化にする必要もあるので、こちらの値をfalseにしています。

詳しくは、PHP IntelliSense - Visual Studio MarketplaceのInstallationを参照してください。

解説2:パフォーマンス低下への対策

規模の大きいPHPプロジェクトの場合、標準設定のままだとCPUの使用量が上がり動作が遅くなりがちになるため、
いくつかその対策となる設定を加えることをお勧めします。

今回は、files.watcherExcludevendorディレクトリ配下のソースコードをファイル監視の対象から除外することにしました。
files.excludeでエクスプローラー上から非表示にする方法もありますが、パフォーマンスが深刻すぎない限り、
そこまでする必要なないかと個人的に思っています。
(むしろ、そこまで大きな規模の場合は、EclipseやNetBeansを使いたい。)

tasks.json: エディタに登録する自動化タスクの設定

Visual Studio Codeで開発を進めていく中で、エディタから実行したい特定のタスクを設定します。

Apache連携で実行されるPHPコードをデバッグするためには、
DocumentRoot(デフォルトは~\scoop\persist\apache\htdocs配下)にソースコードをコピーする必要があります。
そこでデバッグ開始時に実行するそのコピー処理をtasks.jsonに設定します。

sample_project\.vscode\tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Deploy under Apache htdocs",
      "type": "process",
      "command": "powershell",
      "args": [
        "-ExecutionPolicy",
        "Unrestricted",
        "-NoProfile",
        "-Command",
        "Remove-Item \"$(scoop which httpd | split-path)\\..\\htdocs\\*\" -Recurse -Force;",
        "Copy-Item -Recurse * -Exclude *.vscode, composer.* \"$(scoop which httpd | split-path)\\..\\htdocs\" -Force"
      ],
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "presentation": {
        "reveal": "always",
        "panel": "new",
        "echo": true
      }
    }
  ]
}

これで設定完了です。PowerShellで書けるので、かなり柔軟な対応ができるのがいいですね。
次にデバッグ実行の際にtasks.jsonで定義したタスクを実行できるようにします。

launch.json: デバッグ実行の設定

F5でデバッグ開始する際に必要な設定をします。

sample_project\.vscode\launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Listen for XDebug",
      "type": "php",
      "request": "launch",
      "preLaunchTask": "Deploy under Apache htdocs",
      "pathMappings": {
        "${env:USERPROFILE}\\scoop\\persist\\apache\\htdocs": "${workspaceFolder}"
      },
      "port": 9000
    },
    {
      "name": "Launch currently open script",
      "type": "php",
      "request": "launch",
      "program": "${file}",
      "cwd": "${fileDirname}",
      "port": 9000
    }
  ]
}

ちなみにですが、デバッグ実行したい場合は「フォルダーを開く(Ctrl+K, O)」でプロジェクトを開く必要があります。
デバッグしたい単体のファイルを開いただけでは実行できないため注意が必要です。
また、単一スクリプトのデバッグをする場合は、デバッグ対象となるファイルを開いている必要があります。

extensions.json: 推奨する拡張機能の設定ファイル

推奨されている拡張機能を定義します。
一応、既にcodeコマンドでインストール済みですが、設定をgit管理する場合や、
何かの拍子に拡張機能をアンインストールした際にエディタのポップアップで気づくことができるので、
設定しておくと便利です。

sample_project\.vscode\extensions.json
{
  "recommendations": [
    "felixfbecker.php-intellisense",
    "felixfbecker.php-debug"
  ]
}

VS Codeでサンプルアプリを作成してみる

それでは、実際にサンプルのWebアプリを作成します。
今回インストールした全てのツールを動作確認用に使っていきたいため、
「MySQLから取得したデータをComposerでインストールしたTwigを用いてHTMLを生成し、
そのページをApache経由で表示する」だけのコードを書き、
かつ、デバッグも問題なくできるか試していきます。

サンプルコードはPHP Select Data From MySQL - W3Schoolsのソースコートを参考に、簡単なものにしました。

MySQL

PHPからMySQLにアクセスするため、my.iniの編集とテーブル、データの作成を先にしておきます。

~\scoop\apps\mysql\current\my.ini
[mysqld]
datadir=C:/Users/xxxxx/scoop/persist/mysql/data
+character-set-server=utf8
[client]
user=root
+default-character-set=utf8

次にMySQLサーバーを起動します。
単純に実行してしまうと余計なターミナルウィンドウが増えてしまうので、バッググラウンド実行で起動しています。

PowerShell
# mysqldをバックグランドで実行。(ターミナルを終了すれば自動で終了するため、停止方法については割愛します)
Start-Job {mysqld}

#=> Id     Name            PSJobTypeName   State         #=> HasMoreData     Location             Command
#=> --     ----            -------------   -----         -----------     --------             -------
#=> 1      Job1            BackgroundJob   Running       #=> True            localhost            mysqld

mysql -u root
#=> Welcome to the MySQL monitor.  Commands end with ; or \g.
#=> ...

データベース、テーブル、サンプルデータを作成していきます。

PowerShell(MySQL)
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'P@ssw0rd';

CREATE DATABASE sample_project;

USE sample_project;

CREATE TABLE guests (
  id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY, 
  firstname VARCHAR(30) NOT NULL,
  lastname VARCHAR(30) NOT NULL,
  email VARCHAR(50),
  created_at TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO guests (firstname, lastname, email, created_at) VALUES ('太郎', '田中', 'tanaka@example.com', CURRENT_TIMESTAMP);
INSERT INTO guests (firstname, lastname, email, created_at) VALUES ('花子', '山田', 'yamada@example.com', CURRENT_TIMESTAMP);
INSERT INTO guests (firstname, lastname, email, created_at) VALUES ('稲造', '新渡戸', 'nitobe@example.com', CURRENT_TIMESTAMP);

SELECT * FROM guests;
--#=> +----+-----------+----------+-------------------+---------------------+
--#=> | id | firstname | lastname | email             | created_at          |
--#=> +----+-----------+----------+-------------------+---------------------+
--#=> |  1 | 太郎      | 田中      | john@example.com  | 2019-02-08 12:12:52 |
--#=> |  2 | 花子      | 山田      | mary@example.com  | 2019-02-08 12:12:52 |
--#=> |  3 | 稲造      | 新渡戸    | julie@example.com | 2019-02-08 12:12:52 |
--#=> +----+-----------+----------+-------------------+---------------------+

-- デフォルトはオートコミットなので、本当は不要ですがあえて書いておきます。
COMMIT;

-- MySQLを終了するときはexit(または、quitでも可)
exit

上記の手順通りに実行した場合、次回以降MySQLにログインする際は以下のようなコマンドになります。

PowerShell
mysql -u root -pP@ssw0rd sample_project

補足1:MySQL Workbenchでの利用方法

mysqlコマンドではなくGUIで操作したい場合はMySQL Workbenchでも可能です。
特に詳しくは解説しませんが、以下その手順です。

  1. 「スタートメニュー」→「Scoop Apps」→「MySQL Workbench」で起動
  2. MySQL Connections横の「+」ボタンをクリック
  3. 接続情報を以下のように設定し、「Test Connection」で成功すれば、「OK」をクリック image.png
  4. 「sample_project」をクリックすると接続でき、以下のようにクエリなどを実行できるようになります。 image.png

PHPコード

先ほど作成したテーブルのデータを表形式でそのまま出力するだけの処理を実装します。

はじめにTwigを自動起動させるためにプロジェクト直下にbootstrap.phpを作成します。

bootstrap.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

$loader = new Twig_Loader_Filesystem(__DIR__ . '\templates');
$twig = new Twig_Environment($loader);

続いて、ブラウザからアクセス先となるindex.phpを作成します。

public\index.php
<?php

require_once __DIR__ . '/../bootstrap.php';

try {
  $pdo = new PDO('mysql:dbname=sample_project;host=localhost;charset=utf8mb4', 'root', 'P@ssw0rd', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  ]);
  $rows = $pdo->query("SELECT id, firstname, lastname FROM guests")->fetchAll();
} catch (PDOException $e) {
  header('Content-Type: text/plain; charset=UTF-8', true, 500);
  exit($e->getMessage());
}
header('Content-Type: text/html; charset=utf-8');

echo $twig->render('index.html', ['rows' => $rows]);

最後にTwigのテンプレートとなるindex.htmlを作成します。

templates\index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Sample Project</title>
  </head>
  <body>
    <table border="1" style="width: 80%;">
      <tr>
        <th>Id</th>
        <th>Firstname</th>
        <th>Lastname</th>
      </tr>
      {% for row in rows %}
      <tr>
        <td>{{ row.id }}</td>
        <td>{{ row.firstname }}</td>
        <td>{{ row.lastname }}</td>
      </tr>
      {% endfor %}
    </table>
  </body>
</html>

用意するファイルは以上です。
それでは、Apacheを起動します。こちらもバッググラウンド実行です。

PowerShell
Start-Job -Name httpd {httpd}

デバッグ実行

Visual Studio Codeの左のナビゲーションペインに表示されている「デバッグ」をクリックし、
デバッグの構成が"Listen for XDebug"になっていることを確認します。

なっていることが確認できたら、デバッグ実行ボタン(または、F5)を押下します。

http://localhost/index.php にアクセスすると、以下のページが表示できます。

image.png

試しにブレークポイントをindex.phpの17行目に設定します。
ブレークポイントを貼る際は、行ナンバーが表示されているところをクリック、または、F9で可能です。

ブレークポイントの箇所でちゃんと停止でき、変数も確認できます。

image.png

PHPのバージョン管理

今回PHP7.2で構築しましたが、将来的にPHP7.3に更新するかもしれませんし、
違うプロジェクトで古いバージョンを使っているなどあれば、適宜切替したい状況もあるかと思うので、
その際に使えるコマンドを残しておきます。

記事を書いている段階では、まだPHP7.3のXdebugがリリースされていないため、
PHP7.1にバージョンを下げる手順にしました。
※アップデートもまたその逆も手順自体は変わりません。

バージョンの更新方法(PHP7.2→PHP7.1)

全部コマンド化しましたが、結構長くなったのでps1スクリプトにした方がいいかもしれません。

PowerShell
# 関数を宣言
# env, ensure_in_pathも定義済みであることが前提です。
# もし、コンソールを一度終了していれば、再度定義してください。
function strip_path($orig_path, $dir) {
    if($null -eq $orig_path) { $orig_path = '' }
    $stripped = [string]::join(';', @( $orig_path.split(';') | Where-Object { $_ -and $_ -ne $dir } ))
    return ($stripped -ne $orig_path), $stripped
}

function remove_from_path($dir,$global) {
    $dir = (Resolve-Path $dir).Path
    # future sessions
    $was_in_path, $newpath = strip_path (env 'path' $global) $dir
    if($was_in_path) {
        env 'path' $global $newpath
    }

    # current session
    $was_in_path, $newpath = strip_path $env:PATH $dir
    if($was_in_path) { $env:PATH = $newpath }
}

# PHP7.1をインストール
scoop install php71 php71-xdebug

# ユーザー環境変数に登録しているPHPIniDirをPHP7.2から7.1に切り替える。
remove_from_path ~\scoop\apps\php72\current
ensure_in_path ~\scoop\apps\php71\current
scoop which php
#=> C:\Users\xxxxx\scoop\apps\php71\current\php.exe

# custom.iniを移行
cp ~\scoop\persist\php72\cli\conf.d\custom.ini ~\scoop\persist\php71\cli\conf.d\custom.ini
# xdebug.iniを移行
cp ~\scoop\persist\php72\cli\conf.d\xdebug.ini ~\scoop\persist\php71\cli\conf.d\xdebug.ini
replaceFileContentWithoutBOM ~\scoop\persist\php71\cli\conf.d\xdebug.ini 'php72' 'php71'

# httpd.confに指定しているphpのパスをphp72からphp71に変更する
replaceFileContentWithoutBOM ~\scoop\persist\apache\conf\httpd.conf 'php72' 'php71'

# 動作確認
php --ini
#=> PHP Warning:  PHP Startup: Unable to load dynamic library 'ext\curl' - 指定されたモジュールが見つかりません。
#=>  in Unknown on line 0
#=> PHP Warning:  PHP Startup: Unable to load dynamic library 'ext\mbstring' - 指定されたモジュールが見つかりません。
#=>  in Unknown on line 0
#=> PHP Warning:  PHP Startup: Unable to load dynamic library 'ext\openssl' - 指定されたモジュールが見つかりません。
#=>  in Unknown on line 0
#=> PHP Warning:  PHP Startup: Unable to load dynamic library 'ext\pdo_mysql' - 指定されたモジュールが見つかりません。
#=>  in Unknown on line 0
#=> PHP Warning:  PHP Startup: Unable to load dynamic library 'ext\pdo_sqlite' - 指定されたモジュールが見つかりません。
#=>  in Unknown on line 0
#=> Configuration File (php.ini) Path: C:\WINDOWS
#=> Loaded Configuration File:         (none)
#=> Scan for additional .ini files in: #=> C:\Users\xxxxx\scoop\apps\php71\current\cli;C:\Users\xxxxx\scoop\apps\php71\current\cli\conf.d;
#=> Additional .ini files parsed:      C:\Users\xxxxx\scoop\apps\php71\current\cli\php.ini,
#=> C:\Users\xxxxx\scoop\apps\php71\current\cli\conf.d\custom.ini,
#=> C:\Users\xxxxx\scoop\apps\php71\current\cli\conf.d\xdebug.ini

通常は上記の手順で問題ないですが、PHP7.2からphp.iniで指定するextension名の命名規則が一気に変わったため、
上記のようなエラーが出てしまいました。
こういったケースでは、~\scoop\persist\php71\cli\php.iniを開き、正しいextension名を指定しなおしてください。

~\scoop\persist\php71\cli\conf.d\custom.ini
extension=php_curl.dll
extension=php_mbstring.dll
extension=php_openssl.dll
extension=php_pdo_mysql.dll
extension=php_pdo_sqlite.dll
; 以下、省略
PowerShell
php --ini
#=> Configuration File (php.ini) Path: C:\WINDOWS
#=> Loaded Configuration File:         (none)
#=> Scan for additional .ini files in: #=> C:\Users\xxxxx\scoop\apps\php71\current\cli;C:\Users\xxxxx\scoop\apps\php71\current\cli\conf.d;
#=> Additional .ini files parsed:      C:\Users\xxxxx\scoop\apps\php71\current\cli\php.ini,
#=> C:\Users\xxxxx\scoop\apps\php71\current\cli\conf.d\custom.ini,
#=> C:\Users\xxxxx\scoop\apps\php71\current\cli\conf.d\xdebug.ini

バージョンの切替方法

更新時とは異なり、設定ファイルに関してはphp.ini関連は不要でhttpd.confの書き換えのみとなります。

PowerShell
# php72-xdebugはreset対象となるファイル(実行ファイルや環境変数など)がないため、指定不要です。
scoop reset php72
remove_from_path ~\scoop\apps\php71\current
ensure_in_path ~\scoop\apps\php72\current

# httpd.confに指定しているphpのパスをphp71からphp72に変更する
replaceFileContentWithoutBOM ~\scoop\persist\apache\conf\httpd.conf 'php71' 'php72'

# 動作確認
php --ini
#=> Configuration File (php.ini) Path: C:\WINDOWS
#=> Loaded Configuration File:         (none)
#=> Scan for additional .ini files in: #=> C:\Users\xxxxx\scoop\apps\php72\current\cli;C:\Users\xxxxx\scoop\apps\php72\current\cli\conf.d;
#=> Additional .ini files parsed:      C:\Users\xxxxx\scoop\apps\php72\current\cli\php.ini,
#=> C:\Users\xxxxx\scoop\apps\php72\current\cli\conf.d\custom.ini,
#=> C:\Users\xxxxx\scoop\apps\php72\current\cli\conf.d\xdebug.ini

気になったところ

開発環境構築という目的には支障あまりないですが、作業中に遭遇して解決できなかった挙動についてメモします。

  • PHPのビルドインWebサーバーだけでデバッグができない。
     → NetBeansだとできるのですが、VSCodeだと上手くできないようです。
  • httpdをWindowsのサービスとして起動した場合、実行ができない。
     → Apache自体の起動はできているのですが、PHPのWebアプリは動作できていないようでした。
       PHPディレクトリをシステム環境変数のPATHに追加しても挙動変わらず。
       ただ、開発環境構築という目的でサービス起動はあまり用途がないと思うので、詳細は追ってません。
  • 例えば、array_key_existsfile_existsなど、PHP標準の関数の実装を見たい場合、NetBeans等のIDEでは
    宣言場所(php.php)に移動できるようソースアタッチができるのですが、Visual Studio Codeだとそれが難しいようです。

参考

【PHP】XAMPP + VSCodeでPHPの開発環境を作った(Windows) - ぺやろぐ
Custom PHP configuration · lukesampson/scoop Wiki · GitHub
Apache with PHP · lukesampson/scoop Wiki · GitHub
[PHP] Xdebug のリモートデバッグ、理解していますか? - Qiita
PHP開発でComposerを使わないなんてありえない!基礎編 - Qiita
MySQL8.0 認証方式を変更する(Laravel5) - Qiita
PHP Select Data From MySQL - W3Schools
PHPでデータベースに接続するときのまとめ - Qiita
PHPでWebアプリ開発!人気テンプレートエンジン「Twig」を使ってみよう


  1. 実際には設定ファイルを作成する手順をコマンドで書いてないのですが、PowerShellで全てやろうとすると、UTF-8で書き込んだ場合にBOMが付いてしまいます。もちろん、それを付けないように作成する方法もあるのですが、それらを記事に記載するとややこしくなるので、今回は設定ファイルをコマンドで作成する手順を排除しました。例えば、職場で開発環境をスクリプトで一気に作成できるようにする場合はPowerShellでBOM無しUTF8を簡単に扱う、デフォルト設定を簡単に変える方法を参考にすれば、問題なく実施できると思います。 

  2. 上記の方法だとセキュリティレベルが低下してしまうため、それが気になる方はエラーが出るたびにカスペルスキーのデスクトップ通知が来ると思うので、そちら経由で除外リストにその都度除外するホスト名を登録する方法でも問題ないかと思います。 

  3. 元々はこちらのIssuesで提供されたスクリプトになります。PHP8系になると、また同じ問題が起きそうです。 

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

PHPでセッションの長さを変更する

セッションの長さを確認する機会があったけど、どういう仕組みで削除されるのかもわかっていなかったのでまとめました。
PHPでのセッションの使い方については基本的な使用法に記載されています。

PHPのセッションの保持期間

調べるとデフォルトでは下記のようです。
このデフォルト値がphp.iniに定義されており、こちらを変更することでセッションの保持期間を変更できます。

デフォルトでは、1/100の確率で、24分より古いセッションファイルが消えます。

24分よりも古くなったら自動的に削除される訳ではなく、1/100の確率でGC(ガーベッジコレクション)が動き削除を行うようです。
また、この24分の間に再度リクエストがあった場合には、有効期限は更新されます。

参考
セッションの有効期間とか設定とか挙動とかを調べました

デフォルト値の確認

念の為デフォルト値になっているかの確認を行います。php.iniの場所に関しては下記のコマンドでわかるはず。

$ php --ini | grep php.ini
Configuration File (php.ini) Path: /etc
Loaded Configuration File:         /etc/php.ini

確認すべき項目は4つです。

session.gc_maxlifetime

サーバに保存されているセッションファイルの保存期間です。

session.gc_maxlifetime は、データが 'ごみ' とみなされ、消去されるまでの秒数を指定します。 ガベージコレクション (ごみの収集) は、 セッションの開始時に行われます

$ grep "session.gc_maxlifetime" /etc/php.ini
session.gc_maxlifetime = 1440
;       setting session.gc_maxlifetime to 1440 (1440 seconds = 24 minutes):

session.cookie_lifetime

ブラウザのクッキーの有効期間です。先ほどの session.gc_maxlifetime のみ変更をしても、デフォルトだとブラウザを閉じたらセッションが破棄されます。

session.cookie_lifetime は、 ブラウザに送信するクッキーの有効期間を秒単位で指定します。 0 を指定すると "ブラウザを閉じるまで" という意味になります。 デフォルトは、0 です。

$ grep "session.cookie_lifetime" /etc/php.ini
session.cookie_lifetime = 0

session.gc_probability

「1/100」の1の部分ですね。基本的にこちらは変更しなくて良いでしょう。

session.gc_probabilityと session.gc_divisorの組み合わせでgc (ガーベッジコレクション)ルーチンの始動を制御します。 デフォルトは、1 です。

$ grep "session.gc_probability" /etc/php.ini
; gc_probability/gc_divisor. Where session.gc_probability is the numerator
session.gc_probability = 1
; gc_probability/gc_divisor. Where session.gc_probability is the numerator and

session.gc_divisor

「1/100」の100の部分ですね。こちら確認をしたら調べた環境では1000になってました。そのため、各リクエスト毎に0.1%の確率でGCプロセスが始動する状態です。

session.gc_divisorと session.gc_probabilityの組み合わせで すべてのセッションの初期化過程でgc(ガーベッジコネクション)プロセス も始動する確率を制御します。確率は gc_probability/gc_divisor で計算されます。例えば、1/100は各リクエスト毎に1%の確率でGCプロセスが 始動します。 session.gc_divisorのデフォルトは100です。

$ grep "session.gc_divisor" /etc/php.ini
; session.gc_divisor
; when the session.gc_divisor value is 100 will give you approximately a 1% chance
; session.gc_divisor is the denominator in the equation. Setting this value to 1
; when the session.gc_divisor value is 100 will give you approximately a 1% chance
session.gc_divisor = 1000

変更を行う

検証でひとまず3日ぐらいセッションが保存されるように変更をしてみます。(日数は適当です)
クッキーの有効期限とセッションファイルの保存期間
ansibleなどを使用している場合には直接変更しちゃダメですが、そうでない人はひとまずバックアップとしてコピーしておきましょう。

$ sudo cp -pi /etc/php.ini /etc/php.ini.20190117
$ sudo vim /etc/php.ini
$ diff /etc/php.ini /etc/php.ini.20190117
1385c1385
< session.cookie_lifetime = 259200
---
> session.cookie_lifetime = 0
1432c1432
< session.gc_maxlifetime = 259200
---
> session.gc_maxlifetime = 1440
$ sudo service httpd graceful
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP7+PHPUnitで自前のセッションハンドラクラスのテストを書く

  • セッションハンドラを自作した
  • セッションIDが更新されるかとか、古いセッションがちゃんと破棄されるかとかテストしたい
  • PHPUnitの@runInSeparateProcessアノテーションをつければテストできるよ

サンプルコード

<?php

class MySessionHandlerTest extends PHPUnit\Framework\TestCase
{
    /**
     * @runInSeparateProcess
     */
    public function testCreateWriteAndSave()
    {
        // TODO 初めてアクセスした時にセッションが生成されて保存されるテスト
        // @runInSeparateProcess を指定しているので、テスト終了後に自動的にsaveされる
    }

    /**
     * @runInSeparateProcess
     * @depends testCreateWriteAndSave
     */
    public function testLoadReWriteAndSave()
    {
        // @depends を指定しているので、testCreateWriteAndSave() の結果を受けてテストが実行される
        // TODO 2回目にアクセスした時に、セッションIDが付け替えられるテスト
        // TODO 前回のセッションデータがロードできるテスト
        // TODO セッションデータが上書きできるテスト
        // @runInSeparateProcess を指定しているので、テスト終了後に自動的にsaveされる
    }

    /**
     * @runInSeparateProcess
     * @depends testLoadReWriteAndSave
     */
    public function testReLoadAndClear()
    {
        // @dependsを指定しているので、testLoadReWriteAndSave() の結果を受けてテストが実行される
        // TODO 前回のセッションデータをロードしてクリアするテスト
        // @runInSeparateProcess を指定しているので、ここでプロセスが終了し自動的にsaveされる
    }

    /**
     * @runInSeparateProcess
     * @depends testReLoadAndClear
     */
    public function testDestroy()
    {
        // @dependsを指定しているので、testReLoadAndClear() の結果を受けてテストが実行される
        // TODO 前回のセッションデータがクリアされているテスト
        // TODO サーバサイドのセッションを破棄するテスト
        // @runInSeparateProcess を指定しているので、ここでプロセスが終了し自動的にsaveされる
    }

    /**
     * @runInSeparateProcess
     * @depends testDestroy
     */
    public function testDestroyed()
    {
        // @dependsを指定しているので、testDestroy() の結果を受けてテストが実行される
        // TODO セッションが残っていないことを確認するテスト
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PDOでコネクションロスト後にDBに再接続する

要約

アプリケーションサーバが重くなって、DBにコネクションが溜まって、サービス全体がダウン、なんてことにならないように、DBコネクションにwait_timeoutを設定するのは良いことです。
しかし、PDOではコネクションが切れてしまったときに自動で再接続する手段が提供されていません。
そこで、PDOのラッパークラスを作って、再接続できるようにしました。

環境

  • PHP7.0以上
  • MySQL(MariaDB10.1)

ハマりどころ

PDOをラップして、PDO::exec()の時に例外が発生したらcachして、ロストコネクションだったら再接続する、っていうだけなら簡単なんですが、このやりかただとPDO::prepare()->execute()されたときにcatchできなくなります。
execute()PDOStatementのメソッドだからです。

解決策

PDOのラッパークラス自身にタイマーを持たせて、wait_timeoutが経過したら無駄にクエリを1回発行して例外を発生させることで、かならずラッパークラス自身で再接続するようにしました。
ダサいとか言わないで。

サンプルコード

<?php

class ReConnectablePdo
{
    /** @var \PDO $pdo */
    private $pdo;

    private $dsn;
    private $username;
    private $password;
    private $options;
    private $timeout;
    private $lastExecMicroTime;

    /**
     * ReConnectablePdo constructor.
     * @param string $dsn
     * @param string $username
     * @param string $password
     * @param array $options
     * @param int|null $timeout
     */
    public function __construct($dsn, $username, $password, $options, $timeout)
    {
        $this->dsn = $dsn;
        $this->username = $username;
        $this->password = $password;
        $this->options = $options;
        $this->timeout = $timeout;

        $this->connect();
    }

    /**
     * @param $statement
     * @return int
     * @throws RuntimeException
     */
    public function exec($statement)
    {
        return $this->callPDOMethod('exec', [$statement]);
    }

    // こんな感じで各メソッドをラップする

    /**
     * @param string $methodName
     * @param array $params
     * @return int|mixed
     * @throws RuntimeException|\Exception
     */
    private function callPDOMethod($methodName, $params = [])
    {
        $retried = false;
        while (true) {
            try {
                // PDOから直接DBにアクセスせずPDOStatement経由でアクセスする場合があるので
                // クラス内でもTimeoutを監視しておく
                // 前回実行時からTimeout以上経過していた場合は
                // PDO経由でダミーのクエリを発行し再接続を促す
                if ($this->timeout && $this->lastExecMicroTime + $this->timeout - 0.1 <= microtime(true)) {
                    $this->pdo->exec('SELECT 1');
                }
                $ret = call_user_func_array([$this->pdo, $methodName], $params);
                $this->lastExecMicroTime = microtime(true);
                return $ret;
            } catch (\Exception $exception) {
                // 2連続でロストコネクション、またはロストコネクション以外の場合は例外を投げる
                if ($retried || !self::causedByLostConnection($exception)) {
                    throw $exception;
                }
                $this->connect();
                $retried = true;
            }
        }
        // ここには来ない
        return 0;
    }

    /**
     * @return void
     */
    private function connect()
    {
        if (isset($this->pdo)) {
            unset($this->pdo);
        }

        $this->pdo = new \PDO($this->dsn, $this->username, $this->password, $this->options);

        if ($this->timeout) {
            $this->pdo->exec("SET wait_timeout = {$this->timeout}");
        }

        $this->lastExecMicroTime = microtime(true);
    }

    /**
     * @param \Exception $exception
     * @return bool
     */
    private static function causedByLostConnection(\Exception $exception)
    {
        $errorMessagePatterns = [
            'server has gone away',
            'no connection to the server',
            'Lost connection',
            'is dead or not enabled',
            'Error while sending',
            'decryption failed or bad record mac',
            'server closed the connection unexpectedly',
            'SSL connection has been closed unexpectedly',
            'Error writing data to the connection',
            'Resource deadlock avoided',
        ];

        $message = $exception->getMessage();

        foreach ($errorMessagePatterns as $pattern) {
            if (stripos($message, $pattern) !== false) {
                return true;
            }
        }
        return false;
    }
}

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

ぼくのかんがえたさいきょうのPHPエラーハンドリング

やりたいこと

  • Fatal Errorはログを吐いてエラーページを表示したい
  • 誰にもcatchされなかった例外も、ログを吐いてエラーページを表示したい
  • 処理が続行可能なエラーはログだけ吐いて処理を続けたい
  • @演算子でエラーを抑制している場合は、ログに残らないようにしたい
  • エラーハンドラが宣言される前におきた処理続行可能なエラーも、後からログに残しておきたい

注意事項

PHP7未満ではエラーハンドリングの動作が大きく違うので、注意が必要です。

Fatal Errorが発生したら

Fatal Errorが発生したら、PHPは処理を続けることができないので、処理をシャットダウンしにかかります。このとき、事前にregister_shutdown_function()で終了処理を登録しておくことで最後のひと足掻きができます。終了処理内でエラーを拾って、ログ出力とエラーページの表示を行います。

こんなイメージ

<?php
register_shutdown_function(function(){
    $lastError = error_get_last();
    putLog($lastError);
    if (isFatal($lastError['type'])) {
        showErrorPage();
    }
});

例外が誰にもキャッチされなかったら

throwした例外が誰にもcatchされなかった場合は、最終的にset_exception_handler()で登録しておいた例外ハンドラに拾われます。なので、ここに終了処理を書いておけばOKです。

こんなイメージ

<?php
set_exception_handler(function(Throwable $throwable){
    putLog($throwable);
    showErrorPage();
});

エラーが発生したがFatalではなかった場合

エラーが発生した場合、事前にset_error_handler()でエラーハンドラを登録しておけば、エラー処理を行うことができます。Fatalではないエラーの場合、エラー処理が完了すると、プログラムは元に戻って処理を続行します。エラーハンドラがfalse以外を返した場合は、PHP標準のエラー処理は完全にバイパスされます。

こんなイメージ

<?php
set_error_handler(function($severity, $message, $file, $line){
    putLog($severity, $message, $file, $line);
    return true;
});

エラーが発生したが@演算子でエラーが抑制されていた場合

@演算子でエラーを抑制していても、set_error_handler()でエラーハンドラが登録されていた場合は、エラー処理が実行されます。@演算子に頼らないのが一番ですが、エラーログを吐かないようにあえて@演算子をつけたい場合もないわけではありません。
そこで、エラーハンドラ内で@演算子がついている場合はログを吐かないように制御します。これにはerror_reporting()関数を使います。@演算子が効いている間はこの関数の戻り値が0になります。

こんなイメージ

<?php
set_error_handler(function($severity, $message, $file, $line){
    if (error_reporting()) {
        putLog($severity, $message, $file, $line);
    }
    return true;
});

エラーハンドラが宣言される前にエラーがおきた場合

error_get_last()関数を使うことで、最後におきたエラーを取得することができます。エラーハンドラを宣言するときについでにこの関数を呼ぶことで、確実にエラーログに残せるようになります。

全部を網羅したさいきょうのエラーハンドラ

<?php

class ErrorHandler
{
    private $lastMessage = '';

    /**
     * ErrorHandler constructor.
     */
    public function __construct()
    {
        // エラーハンドラが宣言される前にエラーが起こっていたらログに残す
        $lastError = error_get_last();
        if ($lastError) {
            $logMessage = self::buildMessageForPHPError($lastError);
            $this->putLog($lastError['type'], $logMessage, $lastError['file'], $lastError['line']);
            $this->lastMessage = $lastError['message'];
        }

        set_exception_handler([$this, 'exceptionHandler']);
        set_error_handler([$this, 'errorHandler']);
        register_shutdown_function([$this, 'onShutdown']);
    }

    /**
     * ここに来る時は処理続行不可能なので、
     * エラーログを吐いてエラーページを表示する
     *
     * @param Throwable $throwable
     */
    public function exceptionHandler(Throwable $throwable)
    {
        $logMessage = self::buildMessageForThrowable($throwable);
        // ここの$severityはログの出力の判定に使うイメージ
        // 例外は処理続行できないので、ログ出力のレベルはE_ERRORと同等
        $this->putLog(E_ERROR, $logMessage, $throwable->getFile(), $throwable->getLine());

        $this->lastMessage = $throwable->getMessage();
        $this->showErrorPage();
    }

    /**
     * @param int $severity
     * @param string $message
     * @param string $file
     * @param int $line
     * @return bool
     * @throws ErrorException
     */
    public function errorHandler($severity, $message, $file, $line)
    {
        if (self::isFatal($severity)) {
            // FATAL ERRORは処理の続行が不可能なので、exceptionHandler()に処理を引き継ぐ
            // ただし、おそらくここを通ることはなく、
            // FATAL ERRORが発生する場合は直接onShutdownに行くと思われる
            throw new ErrorException($message, 0, $severity, $file, $line);
        }

        // @演算子でエラーを抑制している場合は error_reporting() の返り値が0になるので、エラーを出力しない
        if (error_reporting()) {
            $logMessage = self::buildMessageForPHPError(
                ['type' => $severity, 'message' => $message, 'file' => $file, 'line' => $line]
            );
            $this->putLog($severity, $logMessage, $file, $line);
        }

        // @演算子でエラーを抑制した場合でも
        // Shutdownハンドラ内で error_get_last() を呼び出すとエラーが取れてしまう。
        // その場合にエラーレポートしないよう、 @演算子の有無にかかわらず$messageは保存しておく
        $this->lastMessage = $message;

        // trueを返すとPHPのエラーハンドラはバイパスされる
        return true;
    }

    /**
     * @return void
     */
    public function onShutdown()
    {
        $lastError = error_get_last();

        if (empty($lastError)) {
            return;
        }
        if ($this->lastMessage == $lastError['message']) {
            // すでにこのエラーはログ出力済みなので何もしない
            return;
        }

        $logMessage = self::buildMessageForPHPError($lastError);
        $this->putLog($lastError['type'], $logMessage, $lastError['file'], $lastError['line']);

        // 処理続行不可能なE_ERRORが発生して、errorHandler()が拾えなかった場合
        // ここにくる
        if (self::isFatal($lastError['type'])) {
            $this->showErrorPage();
        }
    }

    /**
     * @return void
     */
    private function showErrorPage()
    {
        //バッファをOFFにする
        while (ob_get_level() > 0) {
            ob_end_flush();
        }
        /** @noinspection PhpIncludeInspection */
        require '../public/500.php';
    }

    /**
     * @param int $severity
     * @param string $message
     * @param string $file
     * @param int $line
     * @return void
     */
    private function putLog($severity, $message, $file, $line)
    {
        // TODO ログ出力
    }

    /**
     * プログラムの実行が中断される重大なエラーかどうか
     *
     * @param int $severity
     * @return bool
     */
    private static function isFatal($severity)
    {
        return boolval($severity & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR));
    }

    /**
     * @param array $errorDetail
     * @return string
     */
    public static function buildMessageForPHPError($errorDetail)
    {
        $message = ''; // TODO エラーメッセージを組み立てる
        return $message;
    }

    /**
     * @param Throwable $throwable
     * @return string
     */
    public static function buildMessageForThrowable($throwable)
    {
        $message = ''; // TODO エラーメッセージを組み立てる
        return $message;
    }
}

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