ラベル PHP の投稿を表示しています。 すべての投稿を表示
ラベル PHP の投稿を表示しています。 すべての投稿を表示

2016/01/30

paypal月額課金のIPN Listenerの実装

Paypalの月額課金(定期購読)は、管理画面上でボタンのHTMLを作って、それを設置するだけで一応、ユーザから月額課金を実現することは出来るのだが、サービス側でこのユーザがこのコンテンツに月額課金してるよってってのは、IPN Listenerというサーバ側でPaypalのトランザクションを受け取る仕組みを実装してやらないといけない。

Paypal上にこのIPNの仕様のドキュメントはあるはあるのだが、ドキュメントがあまり更新されてなかったり、リンク切れのドキュメントがあったりと、やれやれな状態だが、実装した方法を紹介。

定期購読とIPNのおおまかな流れ

  1. ユーザが設置した定期購読ボタンを押下する
  2. Paypalに遷移し、ユーザがPaypal上で決済を行う
  3. Paypal上の決済完了画面が表示される(自サービスのページには戻されない)
  4. 非同期でPaypalから自サーバのIPN ListenerのAPIがコールされ、トランザクションを受け取る(トランザクションの種類については後述)
  5. 自前のIPN Listenerでは、受けたトランザクションがPaypalからのものであるかどうかを確認するためにPaypalに確認のコールを行う
  6. 妥当性の確認が取れたらユーザの決済が完了したトランザクションを確認したらユーザに課金のサービスを提供できる状態にする(DBの更新とか)
  7. 定期的(ボタンで設定した決済間隔)にユーザの決済処理が行われて、その結果がトランザクションで飛んで来るので、決済されていればサービス延長する。
  8. 定期購読の契約がキャンセルされたら同じくキャンセルのトランザクションが飛んでくるので当該サービスを停止処理を行う。


定期購読ボタン作成時の注意

  • ボタンを作成する際には提供サービスごとに課金額を変えるような場合は、購読IDを指定するか、カスタム値にサービスのIDを入れる。
  • また、Paypalからのトランザクションの戻りがどのユーザの決済かを判断するためにカスタム値にユーザIDなどを入れておく。

サービスIDとユーザIDをボタンのカスタム値としていれる例

<input TYPE="hidden" name="custom" value="<?php echo $serviceid;?>,<?php echo $userid">
※何個もカスタム値を入れるなら、わかりやすくするために、serviceid=1,userid=hoge のような入れ方のほうが良いかも。

IPNトランザクションの種類

subscr_signup(購読の契約が行われた)

例えば、初月無料という定期購読にした場合は、このトランザクションを受けたらサービスを有効にしてやる。

subscr_payment(決済が行われた)

上述の通り、無料期間中にはこのトランザクションは流れてこない。1週間無料なら1週間後に初めて決済処理が行われてこのトランザクションが流れてくる。

subscr_cancel(購読の契約が解除された)

Paypal上で購読の解除が行われるとこのトランザクションが流れてくるので、提供しているサービスの停止処理を行う。

subscr_failed(決済が失敗した)

クレジットカードの有効期限が切れていたりなどで、決済日に決済が行われないとこのトランザクションが流れてくる。

こいつがくせ者で、決済に失敗したからといって購読の契約が解除されたわけではない。提供サービスを停止して、再度、定期購読が申し込める画面にアクセスできるようにしてしまうと、ユーザが2重の契約をしてしまう可能性がある。

クレジットカードを登録しなおしたりで決済が可能になると、subscr_paymentが流れてくるので、failed中はサービスを停止ではなくサスペンド状態という扱いにするなどして、契約がキャンセルされたわけじゃないということを意識しておく必要がある。
subscr_failの状態がどれだけ続いたとしても契約は残っている。契約が解除されたら、必ずsubscr_cancelが流れてくるのでそれまではサスペンドする。

subscr_eot(購読の有効期限切れ)

正直、このトランザクションが流れてくるタイミングが良くわからない。購読の有効期限の管理は、自サービス側で行うだろうから、無視しても大丈夫?
StackOverflowでは、以下の様なことやり取りしているが、うーん、わからん。

TLS1.2対応が必須になりそう

2016年1月時点では、Paypalの本番環境ではTLS1.0でのコール(phpのcurl関数でトランザクションの確認のコール)ができていたが、今年になって?からテスト環境にはTLS1.0でコールするとエラーが返ってきた。
どうもPHP5.3のcurlではTLS1.2のプロトコルに対応していないようなので、サーバのcurlコマンドをアップデートして、exec関数でコールするようにしてみた。

Paypalから受け取ったデータの妥当性のチェック通信を行なう

トランザクションの確認は以下のようにcurlコマンドで以下のように接続した。
function confirmPaypalTransaction($posts) {

        $paypal_domain = "www.paypal.com"; //テスト環境はsandbox.paypal.com

        $req = 'cmd=_notify-validate'; 
    
        foreach ($posts as $key => $value) {
            $value = urlencode(stripslashes($value));
            $req  .= "&".$key."=".$value;
        }
    
        $url = 'https://'.$paypal_domain.'/cgi-bin/webscr';        
        $cmd = '/usr/bin/curl '.$url.' --tlsv1.2 -d "'.$req.'"';
        exec($cmd, $res, $ret);
/* 以下のcurl関数では叩けなくなった(PHP5.3)
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1);
        curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'rsa_aes_128_sha');
        curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
        if ( !$res = curl_exec($ch) ) {
            $this->writeLog("FATAL", "CURL ERROR:". curl_error($ch));
        }
        curl_close($ch);*/
        return $res;
}



2016/01/27

PHPで実行時間を同時に何箇所も計測する

PHPのプログラムのどこが遅いのかデバッグするときにブレイクポイントみたいに何箇所にも計測地点を設定したいことがある。PEARのBenchmarkというライブラリがあるようなのでそれを使ってもいいのだが、クラスや関数を跨いでいたりすると面倒なのでグローバルスコープの変数を使って、以下の様なパフォーマンス計測の関数を作ってみた。


function setBenchMarker() {
    if ( !is_array($GLOBALS["benchmark"]) ) {
        $GLOBALS["benchmark"] = array();
    }
    $trace = debug_backtrace();
    $line = $trace[0]["line"];
    $file = $trace[0]["file"];
    array_push($GLOBALS["benchmark"], array("point"=>$file.":".$line, "time"=>microtime(true)));
}
function getBenchMarkResult($delim="\n") {
    $lasttime = 0;
    $lastpoint = 0;
    $retbuf = "";
    $records = $GLOBALS["benchmark"];
    foreach ( $records as $record ) {
        $point = $record["point"];
        $time  = $record["time"];
        if ( $lasttime != 0 ) {
            $retbuf = "[".$lastpoint."]--[".$point."]  = ".sprintf('%0.5f', $time-$lasttime).$delim;
        }
        $lastpoint = $point;
        $lasttime = $time;
    }
    return $retbuf;
}

使い方

class Hoge {
    public function __construct( ){}
    public function moge() {
        setBenchMarker();
    }
}
function foo() {
    setBenchMarker();
}

setBenchMarker();
foo();
$hoge = new Hoge();
$hoge->moge();
setBenchMarker();

$bench = getBenchMarkResult();
echo $bench;
最終結果を文字列として返しているのはerror_logなどに出力することもあるだろうなので、 中でechoはしないようにしている。

実行結果例

$ ./test.php
[/home/ec2-user/test/test.php:4]--[/home/ec2-user/test/test.php:21]  = 0.087116003
[/home/ec2-user/test/test.php:21]--[/home/ec2-user/test/test.php:30]  = 0.010456085
[/home/ec2-user/test/test.php:30]--[/home/ec2-user/test/test.php:9]  = 0.000001907

2016/01/26

Google Map API で地名から緯度経度を取得する

地名と緯度経度の入力サジェストを作るために、何か良いAPIが無いかを調べてみたところ、GoogleMAPのAPIが使えそうだった。

しかも、API利用のためのキーが必要ない!すばらしい!




print_r(getGoogleGeoCode("東京"));

function getGoogleGeoCode($location, $lang="ja") {
    
    $url = "http://maps.google.com/maps/api/geocode/json?sensor=false";
    $url.= "&language=".$lang;
    $url.= "&address=".urlencode($location);
    $rawdata = file_get_contents($url);
    return json_decode($rawdata,TRUE);
}
第一引数は、地名、第二引数は返ってくる地名の言語(例えば、languageの指定をしないと、入力が東京だとTokyoという結果が返ってきてしまう)

geocode APIで返ってくるデータ

Array
(
    [results] => Array
        (
            [0] => Array
                (
                    [address_components] => Array
                        (
                            [0] => Array
                                (
                                    [long_name] => 東京都
                                    [short_name] => 東京都
                                    [types] => Array
                                        (
                                            [0] => administrative_area_level_1
                                            [1] => political
                                        )

                                )

                            [1] => Array
                                (
                                    [long_name] => 日本
                                    [short_name] => JP
                                    [types] => Array
                                        (
                                            [0] => country
                                            [1] => political
                                        )

                                )

                        )

                    [formatted_address] => 日本, 東京都
                    [geometry] => Array
                        (
                            [bounds] => Array
                                (
                                    [northeast] => Array
                                        (
                                            [lat] => 35.8986468
                                            [lng] => 153.9875217
                                        )

                                    [southwest] => Array
                                        (
                                            [lat] => 24.2242343
                                            [lng] => 138.942758
                                        )

                                )

                            [location] => Array
                                (
                                    [lat] => 35.6894875
                                    [lng] => 139.6917064
                                )

                            [location_type] => APPROXIMATE
                            [viewport] => Array
                                (
                                    [northeast] => Array
                                        (
                                            [lat] => 35.817813
                                            [lng] => 139.910202
                                        )

                                    [southwest] => Array
                                        (
                                            [lat] => 35.528873
                                            [lng] => 139.510574
                                        )

                                )

                        )

                    [place_id] => ChIJ51cu8IcbXWARiRtXIothAS4
                    [types] => Array
                        (
                            [0] => administrative_area_level_1
                            [1] => political
                        )

                )

        )

    [status] => OK
)

サジェストのJavaScriptについてはまた次回。

InstagramのURLからコンテンツのIDを調べる


インスタグラムのAPIを使ってコンテンツIDとコンテンツのURLなどの情報が取得できるのだが、逆にコンテンツのURLがわかっていて、そのURLのコンテンツがどのコンテンツIDと紐付いているのかを推測出来ないかを調べていたが、APIも無いしどうしたものかなと迷っていた。


きっとInstagramのWebページのソースにはあるんじゃないかと当該コンテンツIDで検索してみると、
のところの、一番下のmedia_idと一番上のuser_idを組み合わせて、
[media_id]_[user_id] = 1167164565206018358_212875457

という構成になっていた。
そこで、こんな感じのPHPコードでURLからコンテンツのIDを抽出してみた。


preg_matchのところは1行でも表現できると思うけど、見にくいので別々にした。

2016/01/23

EC2でLet's Encrypt + Nginx を試してみる


Let's Encryptという誰でも無料で使えるSSL/TLS証明書発行サービスがPublic Betaを始めました。
https://letsencrypt.org/


前回、AWSのEC2インスタンスでNgnix+PHP7環境を作ったので、この環境にLet's Encryptの証明書を入れてみます。

Gitクライアントを入れて、letsencryptのライブラリを持ってくる。
 
sudo yum install git
git clone https://github.com/letsencrypt/letsencrypt

Nginxを一旦停止する
 
sudo service nginx stop

証明書を発行する
./letsencrypt-auto certonly --standalone -d [ドメイン名] --debug

※Nginxが既に動いていると怒られる。(stopする)
※ドメインの設定が完了してないと怒られる。(nslookup出来るか確認)

メールアドレス、ドメインを設定する。

Nginxの設定

 
    # HTTPS server
    #
    server {
        listen       443;
        server_name  dev.digitra.net;
        root         html;

        ssl                  on;
        #ssl_certificate      cert.pem;
        ssl_certificate      /etc/letsencrypt/live/[ドメイン名]/cert.pem;
        #ssl_certificate_key  cert.key;
        ssl_certificate_key  /etc/letsencrypt/live/[ドメイン名]/privkey.pem;

        ssl_session_timeout  5m;

        ssl_protocols  SSLv2 SSLv3 TLSv1;
        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers   on;

        #charset koi8-r;

        location / {
                index index.html index.php;
        }

        # redirect server error pages to the static page /40x.html
        #
        error_page 404 /404.html;
        location = /40x.html {
        }

        # redirect server error pages to the static page /50x.html
        #
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        location ~ \.php$ {
            root           html;
            fastcgi_pass   127.0.0.1:9000;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME  /var/www/html$fastcgi_script_name;
            include        fastcgi_params;
        }
   }

Nginxを起動する

$ sudo service ngnix start

証明書を更新する

Let's Encryptの証明書の有効期限は90日となっている。(推奨は60日)
なので、定期的に証明書を更新する必要がある。

以下の様なスクリプトをcronに仕込んで自動更新するようにする。

#スクリプト
/home/ec2-user/letsencrypt/letsencrypt-auto certonly --webroot -w /var/www/html -d [ドメイン名] --renew-by-default --debug
service httpd restart

#cronの設定
00 05 01 * * /home/ec2-user/letsencrypt_renew.sh





EC2にNginxとPHP7で爆速環境を作る

ずっとApacheしか使ってなかったのですが、最近仕事でApacheが詰まりまくるので、前から気になっていたNginx(エンジンエックスって読むんだね)と、最近リリースされたPHP7が従来のPHPと比べて2倍ぐらい早いらしいので、Nginx+PHP7の環境をEC2(t2.micro)に構築してみる。

EC2のインスタンス作成とアップデート

AmazonLinuxのAIMでt2.microインスタンスを作成。
セキュリティグループでは、HTTPを追加する。

インスタンスが生成されたら、SSHでログインし、ミドルウェアパッケージの更新を行う。
$ sudo yum update -y


PHP7のインストール

yumのCentOS6用のリポジトリを追加し、PHP7をインストール。
$ sudo rpm -Uvh https://mirror.webtatic.com/yum/el6/latest.rpm

$ sudo yum install --enablerepo=webtatic-testing php70w php70w-devel php70w-fpm php70w-mysql php70w-mb

Nginxをインストール

$ sudo yum install nginx

/etc/nginx/nginx.conf の以下の項目を修正
        #ドキュメントルートを変更
        root         /var/www/html;

        #indexの設定を追加
        location / {
                index index.html index.php;
        }

        #コメントアウトを外す,ディレクトリを修正
        location ~ \.php$ {
            root           html;
            fastcgi_pass   127.0.0.1:9000;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME  /var/www/html$fastcgi_script_name;
            include        fastcgi_params;
        }


サービスの設定

Nginxの場合、Apacheとは違い、Nginxを起動してもPHPが動くわけではない。
PHP-fpmのプロセスを起動する必要がある。

$ sudo service nginx start
$ sudo service php-fpm start

サーバが再起動した際に自動的にサービスが起動するようにする
$ sudo chkconfig nginx on
$ sudo chkconfig php-fpm on

phpinfoの内容を確認

vi /var/www/html/index.php
<?php
phpinfo();


本当に爆速なのか?

Apache Benchを使ってベンチーマークしてみる。

ベンチマーク用のプログラム(for文で1000回 echoしてみる)
<?php
for ( $i=0; $i < 1000; $i++ ) {
    echo "Hello world! \n";
}
どこか別のサーバからabコマンドで計測する
$ ab -n 10 -c 10 http://dev.digitra.net/bench.php
Requests per second:    688.47 [#/sec] (mean)
ちなみに同じt2.microのEC2でApache+PHP5.3で計測すると
Requests per second:    467.31 [#/sec] (mean)
おお、たしかに早い。もっとクライアント数を増やしてみるとおそらく更に差が顕著になるかと思われ。

参考にしたサイト

・Amazon EC2(Amazon Linux)にPHP7をyumでインストールし、Nginxで表示
http://owani.net/php/php7-nginx/285/