20191011のPHPに関する記事は8件です。

【Laravel5.5】テスト内にメールとSMSのテストを両立させたい

命題

「メール通知のテストをしたい」「SMS通知(自作Notifier)のテストをしたい」といったとき、
モックを使用すると安全に実行ができ、実行結果の取得も容易にすることができます。

\Illuminate\Support\Facades\Mail::fake();
\Illuminate\Support\Facades\Notification::fake();

ところが、下記のように両方を使用してしまうと、
想定したようにはテストが動きません。

Customer.php
class Customer {
    use \Illuminate\Notifications\Notifiable

    public function notify()
    {
        $this->notify(new MyNotifyInstance($this->notifier));
    }
}
CustomerTest.php
class CustomerTest extends TestCase
{
    protected function setUp()
    {
        parent::setUp();
        Mail::fake();
        Notification::fake();
    }

    public function testMailNotification()
    {
        $customer = factory(Customer::class)->create([
            'notifier' => 'mail'
        ]);

        $customer->notify();

        Mail::assertSent(MyCustomerMail::class, 1);  //-> エラー。0件となる
    }

    public function testSMSNotification()
    {
        $customer = factory(Customer::class)->create([
            'notifier' => 'sms'
        ]);

        $customer->notify();

        Notification::assertSentTo(  //->OK
            [$customerForSMS ], MySMSNotification::class
        );
    }
}

解決策

CustomerTest.php
class CustomerTest extends TestCase
{
    protected function setUp()
    {
        parent::setUp();
    }

    public function testMailNotification()
    {
        Mail::fake();  //個別に設定するよう変更

        $customer = factory(Customer::class)->create([
            'notifier' => 'mail'
        ]);
        $customer->notify();
        Mail::assertSent(MyCustomerMail::class, 1);
    }

    public function testSMSNotification()
    {
        Notification::fake();  //個別に設定するよう変更

        $customer = factory(Customer::class)->create([
            'notifier' => 'sms'
        ]);
        $customer->notify();
        Notification::assertSentTo(
            [$customer], MySMSNotification::class
        );
    }
}

原因

Notification::fake() が Mail::fake() を上から潰しているようです。

メール送信処理が発火されず、実行件数が0件となるため、
Mail::fake()を使用する際にはNotification::fake()が実行されないようにします。

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

PHPでファイルをzipにまとめて一括ダウンロードする

こんにちは

今日も今日とて台風19号にも負けずに出勤しています。

午後から休みになるんじゃないかと一抹の希望を持っていましたが、どうやらそれは叶わないようで

iOS, android, API開発に続いて最近は管理画面開発までぶん投げられるようになりました。。。
APIも管理画面もPHPなのであまり抵抗がないのが救いです。

今回は管理画面で大量のmp3をzipにまとめて一括でダウンロードする処理が必要だったのですが、安定の泥沼にどハマりからの給料泥棒をキメてしまったので書き留めます。
ちなみに、mp3のデータを途中でバイナリーにエンコードするので、mp3だけでなく色んなデータに対応できるのではないかと勝手に思ってます。
というわけで書いていきます。

まず前提としてZipArchiveクラスが使えること、です。
これのインストール方法ですが、調べたら出ますが例えばamazon linux(centOSも多分これ?)だと
まずはphpのヴァージョンを確認(7.0だったとする)

$php -v
PHP 7.0.33 (cli) (built: Jan  9 2019 22:04:26) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies

$sudo yum install php70-zip.x86_64
(略)
$sudo service httpd restart

こんな感じで!
もしヴァージョンが7.1だったらsudo yum install php71-zip.x86_64でいけるかと思います。
最後にアパッチを再起動します(アパッチ2ならsudo service apache2 restart)

運がいい人はここで無事にZipArchiveクラスが使えるようになります。

controller.php
// Zipクラスロード
$zip = new ZipArchive();
// Zipファイル名
$zipFileName = "file_" . date("Ymds") .'.zip';
// Zipファイル一時保存ディレクトリ
$zipTmpDir = '/tmp/zip/';

// Zipファイルオープン
$result = $zip->open($zipTmpDir.$zipFileName, ZipArchive::CREATE | ZipArchive::OVERWRITE);
if ($result !== true) {
    // 失敗した時の処理
} 

// 処理制限時間を外す
set_time_limit(0);

// ファイルのパスの数を取得
$file_cnt = count($filepaths);// $filepathsにはファイルのURLが入っているものとします
// zipに複数のファイルを詰めていく
while ($i < $file_cnt) {
    // ここではAPIを叩いて返ってきたデータをファイルにしてZIPに取り入れる処理を書きます
    $filepath = $filepaths[$i];
    $ch = curl_init($filepath);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_NOBODY, 0);
    // データ容量が大きい場合はここのTIMEOUTの時間を調整してください
    curl_setopt($ch, CURLOPT_TIMEOUT, 120);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);

    $output = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    if ($status == 200 && mb_strlen($output) != 0){
        // ファイルの取得に成功したのでzipに詰めてインクリメントします
        $zip->addFromString(basename($filepath), $output);
        $i++;
    } else if ($status == 404){
        // サーバーにファイルが見つからないのでインクリメントします
        $i++;
    } else if ($status == 200 && mb_strlen($output) == 0){  
        // サーバーにファイルがあったが何らかの理由でデータがゼロなのでインクリメントせずに再度APIを叩きます         
    }
    curl_close($ch);
    sleep(1);
    // メモリリーク
    unset($output);
    unset($ch);
}
$zip->close();


// 上記で作ったZIPをダウンロードします。
header("Content-Type: application/zip");
header("Content-Transfer-Encoding: Binary");
// ↓これを書いてるサイトが多かったのですが、これでファイルサイズを指定するとダウンロードが長引くことが多かったのでコメントアウトしました
//header("Content-Length: ".filesize($zipTmpDir.$zipFileName));
header("Content-Disposition: attachment; filename=\"".basename($zipTmpDir.$zipFileName)."\"");
// ファイルを出力する前に、バッファの内容をクリア(ファイルの破損防止)
ob_end_clean();
//readfile($zipTmpDir.$zipFileName);
// これはreadfileの代わりの自作メソッドです(後述)
self::efficient_readfile($zipTmpDir.$zipFileName);

//一時ファイルを削除しておく
unlink($zipTmpDir.$zipFileName);

自作メソッドが下記です。

public static function efficient_readfile($path){
    $handle = fopen($path, "rb");
    while(!feof($handle)){
        print fread($handle, 4096);
        ob_flush();
        flush();
    }
    fclose();
}

readfile()を使うと大きいZIPファイルをダウンロードできないので細切れにする必要があります。
これに触れている記事は全然なかったのですが、GoogleChomeだと大きめのファイルはreadfile()ではエラーがでて落とせませんでした。

あと、
header("Content-Transfer-Encoding: Binary");
ここなのですが、こうしてバイナリーデータにエンコードしなければダウンロードできないという事態に直面しました。
これは僕だけなのでしょうか、これまた検索しても全然出てこずハマりました。

また、メモリリークと書いているところがありますが、僕が担当しているプロジェクトはなにぶん予算の貧弱なプロジェクトなものでAWSのインスタンスの性能は(ry
ともかく、変数をunsetしなければすぐにメモリ不足のエラーが出てしまいます。
これでもメモリリークしてるようでダウンロード数を増やすとエラーが出るので
sudo vi etc/php.ini
で中身のmemory_limitを大きく変更したりしました(根本的な解決になっていませんが)

とまあよしなにやったら何とかなりましたというお話です。

めちゃ汚いコードですが、そのうちUtilにでもまとめてブラックボックス化した方がいい部分が結構あるかもですね

API叩きまくるなら並列処理しろよと言われそうですが、PHPが対応していなかったので勘弁してくださいwww

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

Laravel テスト周りの設定

Laravelを使った開発時のテスト周りの設定の覚え書きです。

環境

・Laravel 5.8.35
・PHP 7.1.3
・PHPUnit 7.5.16

1. テスト用DBの作成

DBのデータを取り扱う処理をテストすることがあると思われますので、テスト用のDBを作成します。
普段の開発で使用しているDBをそのまま使用することもできますが、デメリットが多いのでテストで使用するDBは別に作成するのが無難でしょう。
DBの名前はphpunit.xml(後述)の記述との整合性がとれていればなんでも大丈夫です。

DBを作成したら、必要に応じてmigrationやseedingなどを行います。
このときartisanコマンドを使用すると思いますが、データベースを指定するオプションを忘れないよう注意してください。config/database.phpの'connections'内に追記した項目名(後述)を指定してください。

php artisan migrate --database=[config/database.phpの'connections'内に追記した項目名]
php artisan db:seed --database=[config/database.phpの'connections'内に追記した項目名]

2. config/database.phpにテスト用DBの設定を追加

テスト用DBを作成したら、config/database.phpに設定を追加します。記述場所は、'connections'の配列の中です。今回は開発環境でMySQLを使用していますので、同じくMySQLの設定を追記します。

    'connections' => [

        'sqlite' => [
            'driver' => 'sqlite',
            'database' => env('DB_DATABASE', database_path('database.sqlite')),
            'prefix' => '',
        ],
        //開発用DB
        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],

        'pgsql' => [
            'driver' => 'pgsql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '5432'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8',
            'prefix' => '',
            'schema' => 'public',
            'sslmode' => 'prefer',
        ],

        'sqlsrv' => [
            'driver' => 'sqlsrv',
            'host' => env('DB_HOST', 'localhost'),
            'port' => env('DB_PORT', '1433'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8',
            'prefix' => '',
        ],

        //テスト用DB
        'mysql_test' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => 'test_db',
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],

    ],

上記の'mysql_test'の項目がテスト用DBの設定です。'database'の項目だけテスト用DBの名前に変更して、あとは開発用DBの設定をそのままコピペで基本的には大丈夫みたいです。
今回は設定の項目名を'mysql_test'としましたが、phpunit.xml(後述)の記述との整合性がとれていれば、なんでも大丈夫です。

3. phpunit.xmlの編集

PHPUnitの設定ファイルであるphpunit.xmlを編集します。
タグ内でテスト用のenvやDBなどの設定を記述します。今回は以下のように編集しました。

    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="MAIL_DRIVER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="DB_DATABASE" value="test_db"/>
        <server name="DB_CONNECTION" value="mysql_test"/>
    </php>

"DB_DATABASE"を"test_db"(テスト用に作成したDBの名前)
"DB_CONNECTION"を"mysql_test"(config/database.phpで追記した項目名)
にしました。

テスト用にenvファイルを作成している場合もここで設定できます。
.env.testing などという名前でenvを作成しておき、

<server name="APP_ENV" value="testing"/>

とすると適用されます。テスト用のenvがない場合はデフォルトのenvファイルが適用されます。

これでテストを実行する準備が整いました。?

トラブルシューティング

phpunit.xmlの設定が反映されない?

・テスト用のDBを作成してphpunit.xmlに記述しているのに、テストを実行するとなぜか開発用のDBが使用されている
・テスト用にenvファイルを作成してphpunit.xmlに記述しているのに適用されていない
などの場合

<server name="DB_DATABASE" value="test_db" force="true"/>
<server name="DB_CONNECTION" value="mysql_test" force="true"/>

force="true"のオプションを試してみてください。

force="true"を書いてもphpunit.xmlの設定が反映されない?

キャッシュクリアのコマンドを実行して再度試してみてください

php artisan config:clear

何をやってもテスト用の設定が反映されない?

ググって原因をひとつずつ潰していくしかないです。
この状態に陥ったことがありますが、テスト用DBを作成し直してイチから設定しなおしました。

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

phalcon voltにオリジナルやphpのFuctionを追加する方法

phalconのVoltを拡張

https://phalcon-docs-ja.readthedocs.io/ja/stable/reference/volt.html#id26

ここにあるのですが、
ズバリな回答じゃないので。。

config/services.php

config/service.php
$compiler->addFunction(
    "widget",
    function ($resolvedArgs, $exprArgs) {
        return "MyLibrary\\Widgets::get(" . $resolvedArgs . ")";
    }
);

こんな記述があるんですが、
たぶん、Viewの拡張をするところで、voltの動作を定義しているところがあります。そこに追加する感じになると思います。

:helmet_with_cross:注意点は、実行したいFunction名を文字列で返すというところ。ここ大事

$di->setShared('view', function() {
}

シンプルにphpのdate_format関数を追加する場合

$volt->getCompiler()->addFunction('date_format', 'date_format');

以下、なんかちょっと処理を入れる場合、postされたデータをbase64_decodeを追加する方法

$volt->getCompiler()->addFunction('base64_decode', function($args) {
          $str = str_replace(' ', '+', $args);
          return "base64_decode({$str})";
});

引数がたくさんある場合

$volt->getCompiler()->addFunction('mb_strimwidth', function($args) {
          return "mb_strimwidth({$args})";
});

ここがちょっとおもしろいところで、addFunction のところの第2引数にfunction($args)としてますが、ここは複数引数指定されたらそれらの文字列が渡ってくるのです。

使うときはこんな感じだけど

{{ mb_strimwidth(str,0,10,"...") }}

上記でいう
$args
には、

str,0,10,"..."

という文字列が入ってくるので、そのままセットすればよいです。

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

PHP Slim 環境構築(11) NgnixとPHP-FPMは同じ夢を見るか

PHP Slim 環境構築(11) NgnixとPHP-FPMは同じ夢を見るか

Introduction

前回は、Dockerコンテナとして、NginxとPHP-FPMを別々のコンテナに構築していました。

今回は、NginxとPHP-FPMを同じDockerコンテナの中に格納してみます。

この一連のシリーズは、自分への備忘録が第一目的のため、かなり不親切です。
すみません・・・

なぜ一つのコンテナに入れるのか

AWS ECSでの運用を考えたときに、nginxコンテナとphp-fpmコンテナの両方を管理するという事態を避けたいからです。その理由は、以下の二つです。

  1. ずっと固定数で運用しつづけるのであればそれでも良いのですが、負荷に応じて増減する場合には調整が面倒であること。
  2. nginxとphp-fpm間の通信自体も馬鹿にならないのではないかも? (未調査。AZが分かれた場合はどうなる? または、同一AZを保証する仕組みが必要)

なお、Dockerコンテナには一つのプロセスのみを入れるべしというベストプラクティスには反することになりますが、メリットの方が大きい気がします
(実際にパフォーマンスやコストなどを測定・検討したわけではありません)。

変更点

前回までは、nginxコンテナとphp-fpmコンテナを構築していました。
今回は、nginxコンテナ(リバースプロキシ用)と、nginx+php-fpmコンテナ(バックエンド用)を構築することにします。なお、バックエンド用のnginx+php-fpmの二つのプロセス間の通信はunixドメインソケットを使用します。

ソースツリー

今回の変更箇所は、web_hoge下のみです。

$(PROJECTROOT)
  /compose
    /web_hoge
      Dockerfile
      nginx.conf                (新規)
      nginx-server.conf         (local.confから改名)
      nginx-supervisor.conf     (新規)
      php.ini
      php-fpm.conf              (新規)
      php-fpm-supervisor.conf   (新規)
      supervisord.conf          (新規)

Dockerfile

大幅に書き換わっています。主な変更点は以下の通りです。

  • ベースイメージがphp:7.3.9-fpm-alpine3.10からalpine3.10.2に変更
  • phpおよびそのモジュールのインストールは、peclやdocker-php-ext-enableなどではなく、apkコマンドに変わっています
  • タイムゾーンを明示的にUTCにセット
  • supervisorとその設定のインストール
  • php-fpm用ユーザ(www-data:www-data, uid=1000, gid=1000)と、nginx用ユーザ(nginx:www-data, uid=1001, gid=1000)を追加しています
/compose/web_hoge/Dockerfile
FROM alpine:3.10.2

ARG environment

RUN apk upgrade --update \
  && apk --no-cache --virtual .build-deps add make g++ gcc re2c autoconf curl \
  && apk --no-cache add gettext-dev libzip-dev curl-dev

RUN apk --no-cache add supervisor tzdata nginx php7 php7-fpm \
     php7-gettext php7-mbstring php7-zip php7-ctype php7-json php7-bcmath php7-sockets php7-curl php7-simplexml \
     php7-pecl-apcu php7-pecl-igbinary php7-pecl-msgpack

# PDO(MySQL)
RUN apk --no-cache add mariadb-dev php7-pdo_mysql

# PDO(PostgreSQL)
RUN apk --no-cache add postgresql-dev php7-pdo_pgsql

# YAML (2.0.4)
RUN apk --no-cache add yaml-dev php7-pecl-yaml

# install phpize
RUN apk --no-cache add php7-dev

# redis (5.0.2) extension=redis.so
RUN curl -fsSL https://github.com/phpredis/phpredis/archive/5.0.2.tar.gz -o redis.tar.gz \
  && mkdir -p /usr/src/php/ext/redis \
  && tar xzf redis.tar.gz -C /usr/src/php/ext/redis --strip-components=1 \
  && rm redis.tar.gz \
  && cd /usr/src/php/ext/redis \
  && phpize \
  && ./configure --enable-redis-igbinary \
  && make \
  && make install \
  && echo "extension=redis.so" > /etc/php7/conf.d/80_redis.ini \
  && make distclean

# remove phpize
RUN apk del php7-dev

# xdebug(2.7.2) extension=xdebug.so
RUN if [ "${environment}" = "local" ]; then \
     apk --no-cache add php7-pecl-xdebug \
  && echo "zend_extension=xdebug.so" > /etc/php7/conf.d/xdebug.ini \
  && echo "[XDebug]" >> /etc/php7/conf.d/xdebug.ini \
  && echo "xdebug.remote_enable=1" >> /etc/php7/conf.d/xdebug.ini \
  && echo "xdebug.remote_connect_back=0" >> /etc/php7/conf.d/xdebug.ini \
  && echo "xdebug.remote_autostart=0" >> /etc/php7/conf.d/xdebug.ini \
  ; fi

# TZ
RUN cp /usr/share/zoneinfo/UTC /etc/localtime \
  && echo "UTC" > /etc/timezone

# supervisord
RUN mkdir -p /etc/supervisor.d
COPY supervisord.conf /etc/supervisord.conf

# settings
COPY settings.php /var/www/settings.php

# PHP
COPY php.ini /php.ini
COPY php-fpm.conf /php-fpm.conf
COPY php-fpm-supervisor.conf /php-fpm-supervisor.conf
RUN cp /php-fpm.conf /etc/php7/php-fpm.d/z-php.conf \
  && cp /php.ini /etc/php7/conf.d/config.ini \
  && cp /php-fpm-supervisor.conf /etc/supervisor.d/php-fpm.conf

# Nginx
COPY nginx.conf /nginx.conf
COPY nginx-server.conf /nginx-server.conf
COPY nginx-supervisor.conf /nginx-supervisor.conf
RUN cp /nginx.conf /etc/nginx/nginx.conf \
  && cp /nginx-server.conf /etc/nginx/conf.d/default.conf \
  && cp /nginx-supervisor.conf /etc/supervisor.d/nginx.conf

RUN apk del --purge .build-deps \
  && rm -rf /var/cache/apk/* \
  && rm -rf /tmp/*

RUN deluser nginx \
    && delgroup www-data \
    && addgroup -g 1000 -S www-data \
    && adduser -u 1000 -D -S -G www-data www-data \
    && adduser -u 1001 -D -S -G www-data nginx

EXPOSE 3128

ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

nginx.conf

Nginxのメインの設定ファイル(/etc/nginx/nginx.conf)。
Nginxのdockerイメージから拝借し、以下の箇所を変更しました。

  • Nginxのworker processの実行ユーザをnginx:www-dataに変更 (user nginx www-data;)
  • pidファイルの出力先を指定 (pid /var/run/nginx.pid;)
  • worker process数を1に固定 (コンテナの性能に合わせて調整が必要) (worker_processes 1;)
  • error_logの出力先をSTDERRに指定 (error_log /dev/stderr warn;)
  • access_logの出力を抑制 (#access_log)
/compose/web_hoge/nginx.conf
# /etc/nginx/nginx.conf

user nginx www-data;          ## OVERWRITTEN

pid /var/run/nginx.pid;       ## ADDED

# Set number of worker processes automatically based on number of CPU cores.
# worker_processes auto;
worker_processes 1;           ## OVERWRITTEN

# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;

# Configures default error logger.
# error_log /var/log/nginx/error.log warn;
error_log /dev/stderr warn;   ## OVERWRITTEN

# Includes files with directives to load dynamic modules.
include /etc/nginx/modules/*.conf;


events {
    # The maximum number of simultaneous connections that can be opened by
    # a worker process.
    worker_connections 1024;
}

http {
    # Includes mapping of file name extensions to MIME types of responses
    # and defines the default type.
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Name servers used to resolve names of upstream servers into addresses.
    # It's also needed when using tcpsocket and udpsocket in Lua modules.
    #resolver 208.67.222.222 208.67.220.220;

    # Don't tell nginx version to clients.
    server_tokens off;

    # Specifies the maximum accepted body size of a client request, as
    # indicated by the request header Content-Length. If the stated content
    # length is greater than this size, then the client receives the HTTP
    # error code 413. Set to 0 to disable.
    client_max_body_size 1m;

    # Timeout for keep-alive connections. Server will close connections after
    # this time.
    keepalive_timeout 65;

    # Sendfile copies data between one FD and other from within the kernel,
    # which is more efficient than read() + write().
    sendfile on;

    # Don't buffer data-sends (disable Nagle algorithm).
    # Good for sending frequent small bursts of data in real time.
    tcp_nodelay on;

    # Causes nginx to attempt to send its HTTP response head in one packet,
    # instead of using partial frames.
    #tcp_nopush on;


    # Path of the file with Diffie-Hellman parameters for EDH ciphers.
    #ssl_dhparam /etc/ssl/nginx/dh2048.pem;

    # Specifies that our cipher suits should be preferred over client ciphers.
    ssl_prefer_server_ciphers on;

    # Enables a shared SSL cache with size that can hold around 8000 sessions.
    ssl_session_cache shared:SSL:2m;


    # Enable gzipping of responses.
    #gzip on;

    # Set the Vary HTTP header as defined in the RFC 2616.
    gzip_vary on;

    # Enable checking the existence of precompressed files.
    #gzip_static on;


    # Specifies the main log format.
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
            '$status $body_bytes_sent "$http_referer" '
            '"$http_user_agent" "$http_x_forwarded_for"';

    # Sets the path, format, and configuration for a buffered log write.
    #access_log /var/log/nginx/access.log main;  ## OVERWRITTEN


    # Includes virtual hosts configs.
    include /etc/nginx/conf.d/*.conf;
}

nginx-server.conf

/etc/nginx/nginx.confから呼び出される個別設定ファイル(/etc/nginx/conf.d/default.conf)。

標準的なFastCGI用の設定です。
なお、このサーバがSSLリクエストを直接受けることは無いので、SSL関連の設定はしていません。
また、アクセスログ、エラーログは標準出力、標準エラー出力に出しています。

/compose/web_hoge/nginx-server.conf
server {
  listen 3128;
  server_name _;
  access_log /dev/stdout;
  error_log /dev/stderr warn;
  root /var/www/hoge/public;

  location = /favicon.ico {
    empty_gif;
    access_log off;
  }

  location / {
    try_files $uri $uri/ /index.php?$query_string;
    index     index.php;
  }

  location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param HTTPS $https;
  }
}

nginx-supervisor.conf

supervisorサービス用の設定です(/etc/supervisor.d/nginx.conf)。
この設定によって、nginxがsupervisorによって呼び出されます。
また、nginxプロセス上の標準出力、標準エラー出力は、そのまま、Dockerコンテナの標準出力、標準エラー出力に出力しています。

/compose/web_hoge/nginx-supervisor.conf
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
priority=10
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

php.ini

PHPの上書き用設定ファイルです(/etc/php7/conf.d/config.ini)。
php-fpmの子プロセスのメモリ上限(memory_limit)は、コンテナに合わせて調整が必要です。

/compose/web_hoge/php.ini
[PHP]
log_errors = On
display_startup_errors = On
error_reporting = E_ALL
upload_max_filesize = 100M
post_max_size = 128M
memory_limit = 256M

php-fpm.conf

php-fpm用の上書き用の設定です。最後に読み込まれるようにファイル名を"z-php.conf"になっています。
php-fpmの子プロセスの個数(pm.max_children)は1にしていますが、nginxのworkerプロセスと同様に、コンテナに合わせて調整が必要です。

/compose/web_hoge/php-fpm.conf
[global]
pid = /var/run/php-fpm.pid

[www]
listen = /var/run/php-fpm.sock
listen.owner = www-data
listen.group = www-data
access.log = /dev/null
user = www-data
group = www-data
pm = static
pm.max_children = 1

php-fpm.supervisor.conf

supervisorサービス用の設定です(/etc/supervisor.d/php-fpm.conf)。
この設定によって、php-fpmがsupervisorによって呼び出されます。
また、php-fpmプロセス上の標準出力、標準エラー出力は、そのまま、Dockerコンテナの標準出力、標準エラー出力に出力しています。

/compose/web_hoge/php-fpm.supervisor.conf
[program:php-fpm]
command=/usr/sbin/php-fpm7 --nodaemonize
autostart=true
autorestart=true
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

supervisord.conf

supervisorプロセス用の設定です。Dockerのpid=1プロセスになるので、daemon化しません。

/compose/web_hoge/supervisord.conf
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisord.log
pidfile=/var/run/supervisord.pid

[include]
files=/etc/supervisor.d/*.conf

起動

以前作成したローカル環境と同じです。

$ docker-compose -f docker-compose-common.yml -f docker-compose-local.yml build
$ docker-compose -f docker-compose-common.yml -f docker-compose-local.yml up -d

ここまでのソース

こちらでどうぞ。

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

ブラウザのタブ別にセッションを分ける方法を考えてみた

Webサイトによくある導線として、「入力画面→確認画面→完了画面」というのがあると思います。

  1. 入力画面で入力された内容をセッションに入れて、
  2. 入力内容を確認画面で表示して、
  3. 完了画面でセッションの情報を登録する

という処理が想像できます。

ここでたまに起きる問題として、別タブで同じ画面を開き、タブ同士を行ったり来たりしながら完了画面に行くと確認画面と違う内容が登録されてしまうというもの。

なぜ?

セッションの仕組みはcookieにセッションIDを保存して成り立っているので、通常「1ブラウザ = 1cookie = 1セッション」となります。
だから、ブラウザのタブを複数開いていた場合でも、セッションはタブ毎ではなく全タブで共通となる。

つまり、同じキーでセッションに値を格納したら後勝ち(上書き保存)になるのです。

手順 タブ 操作内容 備考 セッションの中身
タブ1 入力画面を開く - -
タブ1 入力して確認画面に遷移する ここでセッションに入力内容を格納する 入力内容1
タブ2 新しいタブで入力画面を開く - 入力内容1
タブ2 タブ1の時とは違う内容を入力して確認画面に遷移する ここでセッションに入力内容を格納する 入力内容2
タブ1 タブ1に戻って完了画面に遷移する セッションにある入力内容2の情報を登録する 入力内容2

このような手順で操作をした場合、ユーザー的には「タブ1」の確認画面で入力した内容(入力情報1)が表示されているので、当然それが登録されると思うが、実際は「手順4」でセッション内容が「入力内容2」に上書きされているので、最終的には「入力内容2」が登録されている。

対策

そこで、セッションの中身をちょっと工夫してみます。
セッションに格納する時のキーをタブ毎に用意して、その中でさらにkey:valueにしてデータを格納します。

イメージはこんな感じ。

セッションのイメージ.
[
    タブ1のセッションキー => [
     'name' => 'やまだたろう',
     'email' =>'yamada@example.com',
    ],
    タブ2のセッションキー => [
     'name' => 'たなかいちろう',
     'email' =>'tanaka@example.com',
    ],
]

ポイント

  • タブ毎のセッションキーは、ミリ秒単位のタイムスタンプとかUUIDとか絶対に被らないIDを生成する
  • タブ毎に生成したセッションキーを画面に隠し持たせる。<hidden name="tab_session_key" value="【生成したセッションID】">
  • ビジネスロジックでセッションを使うときはリクエストの生成したセンションキーを使ってデータを取得する。(セッションに値を追加する時も)

さらに

  • セキュリティー的には、生成したセッションキーは画面遷移毎とか一定時間毎に生成しなおすと良いかも
  • セッションタイムアウト機能を持たせておいて、一定時間過ぎたタブのセッションは削除する様にするとcookieを圧迫しないで済むかも
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】PHPとAjax(jquery)で進捗状況(プログレスバー)を表示する

概要

  • 社内ツールでビューからオブジェクトを呼び出してデータの挿入をするツールを作りました
  • 処理に時間がかかるためプログレスバーを表示する方向にしました
  • データを送るということが構造上?知識的に難しかったので一時ファイルに吐き出し、それをajaxで一定時間ごとに読み込むという形で実現できました

最終的にどうなったか

  • テスト検証のgifで実際にデータを入れているシーンなので進捗の推移が遅いです。

progress.gif

処理構成(簡潔に)

  1. index.php(ビューとビューからの処理が書かれている)の内容が画面に表示されています
  2. ユーザーは色々とチェックボックスやラジオボタンなどの条件を入れます
  3. 確認ボタンを押すと2.で入力された条件を元にデータを抽出しします(抽出したのは画面に表示)
  4. さらに登録ボタンを押すと、抽出された内容がDBにInsertされ処理が完了するというものです。

データの挿入条件

  • 約8万件の情報
  • DBの負荷を考えて5000件単位でInsert
  • 処理開始から終了まで約15分

挿入中の画面状態

  • 左上のくるくるがずっと出ている感じ

挿入後の画面状態

  • 挿入後は画面が切り替わり、例外等なければ完了メッセージが出る

環境

  • PHP5.2.8
  • RHEL4 、、、社内ツールということもありリプレースの予定もなく古いです。

ajaxで値のやり取りではなくローカルファイルから随時読み込む形に

進捗状況はアップロードばかり?調べてもあまり出てこない

  • 調べても出てくるのはファイルをローカルからサーバーにアップロードするときの進捗状況の表示ばかりでした
  • 大体がビューと同じファイルに書かれた処理の値をjs側で取得している内容でした
  • 別phpファイルにリクエストを投げて取得するのもありましたが今回はオブジェクトとして呼び出したファイルから3ファイル先のオブジェクトを呼び出しそこでInsertするようにしていたので実現したいことと違う処理に感じました

実装(実際よりも簡単にしたのを書きます)

進捗状況までの流れ

1.Insert開始
2.Insertはループになっておりそのなかで進捗状況を上書きで書き出すようにしました

insert.php
foreach ($idData as $id) {
    // 進捗率を求める(小数点以下はfloor()で切り捨て)
    $percent = floor($insertedIdCnt / $allIdCnt * 100);

    // ファイルに書込み
    $fp = fopen('percentNow.log', 'w');
    fwrite($fp, $percent);
    fclose($fp);

    // 挿入処理

    // 1ループ10秒止めておく
    sleep(10);
}
  • この段階で毎ループで進捗率($percent)が上書きされていきます
  1. ビュー側で上で吐き出したファイルを読み込んでいきます(以下ではjsファイルとして書いてますが実際にはphpファイルです
progress.js
var Progress = (function() {
    function Progress(p) {
        this.bar = document.querySelectorAll('#progressBar > .progressBarBody')[0];
        this.p = p;
        this.update();
    }
    Progress.prototype.update = function() {
        this.bar.style.width = this.p + '%';
    }
    Progress.prototype.countup = function(data) {
        if (this.p < 100) {
            this.p = Number(data);
        }
        this.update();
    }
    return Progress;
}());

// 進捗率と進捗バーを更新する部分です
var updateProgress = function(progress) {
    $.ajax('./hogehoge/percent.log', {
        dataType: 'text',
        success: function(data) {
            $('#progress').html('進捗状況: '+data+'%');
                progress.countup(data);
        }
    });
}

// 今回はsubmitが2つあるためclickとボタンのID(#register)で処理します
// 10秒ごとにInsert≒進捗率変更なので1秒ごとに取得すれば十分かなと思いそう設定しています
$('#register').on('click', function() {
    $('#progressArea').html('<div id="progressBar" class="progress"><div class="progressBarBody"></div></div>');
    var progress = new Progress(0);
    $('#progress').html('進捗状況: 0%');
    setInterval(function() {
        updateProgress(progress);
    }, 1000);
});
  1. ビューに表示(これもほんとはphpファイル)
view.html
<!-- 進捗状況表示 -->
<div id="progressVal">
    <span id="progress"></span>
</div>
<div id="progressArea">
</div>
  1. 進捗バーのCSS(デザインに合わせて調整してください)
style.css
.progress {
  width: 60%;
  height: 30px;
  background-color: #F5F5F5;
  border-radius: 4px;
  box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
}

.progressBarBody {
  transition: width 0.5s linear;
  height: 100%;
  background-color: #337AB7;
  border-radius: 4px;
}

参考

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

【Laravel】 Model::firstOrCreate() をバルクインサートで実現する

元ネタ

深夜の思いつきで laravel/ideas から拾ってきて即席で書いてしまった。

スレ主 findOrCreateMany って書いてるけど個人的には bulkFirstOrCreate() のほうが命名近い気がするのでこれで。

やりたいこと

Model::firstOrCreate()1 SELECT + 1 INSERT によって実現されるが,複数レコードに対してやると無駄が多い。そのため,複数レコードを処理するときにも 1 SELECT + 1 INSERT だけで済むようにしたい。

Laravel の Eloquent Model は公式でバルクインサートをサポートしておらず,あくまでサポートしているのは Eloquent Builder と Query Builder のみであるため,タイムスタンプやオートインクリメント値を自分でセットするなど少々工夫が必要。

実装

完全に Model::firstOrCreate() を再現しようとすると困難なので, ユニーク属性は1つ という制約を設けた上で作成する。第1引数にユニークと見なすキーの名前,第2引数に属性群の配列を渡す。

/**
 * Trait BulkFirstOrCreates
 *
 * @mixin \Illuminate\Database\Eloquent\Model
 */
trait BulkFirstOrCreates
{
    /**
     * @param  string                                   $uniqueKeyName
     * @param  array                                    $attributesArray
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public static function bulkFirstOrCreate(string $uniqueKeyName, array $attributesArray)
    {
        $instance = new static();

        // Retrieve actually existing models
        $existingModels = $instance
            ->newQuery()
            ->whereIn($uniqueKeyName, array_column($attributesArray, $uniqueKeyName))
            ->get();

        // Mix timestamp attributes into $attributesArray entries
        if ($instance->usesTimestamps()) {
            $instance->updateTimestamps();
            $attributesArray = collect($attributesArray)
                ->map(function (array $attributes) use ($instance) {
                    return $attributes + $instance->getAttributes();
                })
                ->all();
        }

        // Create new models only from non-existent values
        $instance->newQuery()->insert(
            $nonExistentAttributesArray = collect($attributesArray)
                ->whereNotIn($uniqueKeyName, $existingModels->pluck($uniqueKeyName))
                ->all()
        );

        // Retrieve last insert ID
        $lastInsertId = (int)$instance->getConnection()->getPdo()->lastInsertId();

        // Simulate model hydration without running SELECT query
        $createdModels = $instance
            ->newQuery()
            ->hydrate($nonExistentAttributesArray)
            ->each(function (self $model) use (&$lastInsertId) {
                // Assign auto-increment value
                if ($model->getIncrementing()) {
                    $model->{$model->getKeyName()} = $lastInsertId++;
                    $model->syncOriginal();
                }

                // Fire "eloquent.created" event
                $model->wasRecentlyCreated = true;
                $model->fireModelEvent('created', false);
            });

        // Sort in the $valuesArray order and return as a Collection
        return $instance
            ->newCollection(array_column($attributesArray, null, $uniqueKeyName))
            ->replace($existingModels->keyBy($uniqueKeyName))
            ->replace($createdModels->keyBy($uniqueKeyName))
            ->values();
    }
}

firstOrNew() updateOrCreate() とかも含めて汎用化できそうだったら今後ライブラリ化するかも…
と思ったが, firstOrNew() なんて作っても意味ないし, updateOrCreate() は MySQL 固有文法の ELT FIELD が出てきて作りづらいのでパスかな…

使用例

class PostCode extends Model
{
    use BulkFirstOrCreates;
}
$codes = PostCode::bulkFirstOrCreate('code', [
    ['code' => '2001'],
    ['code' => '2002'],
]);
var_dump($codes[0]->id); // int(1)
var_dump($codes[0]->code); // string(4) "2001"
var_dump($codes[0]->wasRecentlyCreated); // bool(true)
var_dump($codes[1]->id); // int(2)
var_dump($codes[1]->code); // string(4) "2002"
var_dump($codes[1]->wasRecentlyCreated); // bool(true)

$codes = PostCode::bulkFirstOrCreate('code', [
    ['code' => '2000'],
    ['code' => '2001'],
    ['code' => '2002'],
    ['code' => '2003'],
]);
var_dump($codes[0]->id); // int(3)
var_dump($codes[0]->code); // string(4) "2000"
var_dump($codes[0]->wasRecentlyCreated); // bool(true)
var_dump($codes[1]->id); // int(1)
var_dump($codes[1]->code); // string(4) "2001"
var_dump($codes[1]->wasRecentlyCreated); // bool(false)
var_dump($codes[2]->id); // int(2)
var_dump($codes[2]->code); // string(4) "2002"
var_dump($codes[2]->wasRecentlyCreated); // bool(false)
var_dump($codes[3]->id); // int(4)
var_dump($codes[3]->code); // string(4) "2003"
var_dump($codes[3]->wasRecentlyCreated); // bool(true)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む