伊藤清徳の垂直落下式ムーンサルトプレス

PerlとかPHPとかMySQLとか...がんばっても8割だ。

Author: admin (page 1 of 27)

楽天の注文フォームに無茶させるver.2.1 スマホ対応

どうもどうもおひさしぶりです。伊藤です。
全然ブログ書かねぇじゃねぇかとクライアント各位に叱られそうですが、全くそのとおりでございます。

昨年、楽天の注文フォームにむちゃさせるver2という記事を書きました。が、この記事少しだけ不具合がありました。ので、それを修正するお話。

スマホ対応

不具合の具体的な内容は、スマートフォン対応です。スマホの場合は少しだけパラメータが追加になります。

あ、その前にPCの場合の対応は、以前の記事を見てくださいね。

ちょっとだけ難しい話をきかきますが、スマホ時は、以前の方法だと、カゴにはいったと処理完了のステータスが楽天側からリターンがあるにもかかわらず、カゴにいくと中身がなくなってしまうという不具合があります。どうも楽天のスマホのカートシステムは、URIから制御を取得しているらしく、あらかじめ専用セッションにカートの追加命令が保存されていないと、URIの処理が優先されて、カートが空になるという仕様になっているようです。

AJAX送信時にパラメータ追加

結論から言えば、

$.ajax({
  url: 'http://direct.step.rakuten.co.jp/rms/mall/cartAdd/',
  type: 'get',
  dataType: 'jsonp',
  data: {
      'shopid': ショップID(整数値),
      'units': 個数(整数値),
      'itemid': 商品番号(整数値),
      'device': 'sp',//デバイスモードをスマホに
      'userid': 'itempage',//これがないとカゴにはいらないときがある???
      'dbasket_choice_select[]': 'オプション文字列',
      'dbasket_choice_select[]': 'オプション文字列2'
  }
})
.then(function(data){
  if( data.resultCode == '0'){
    alert('OK');
  }
  else{
    alert( 'error!: ' + data.resultMessage );
  }
});

というパラメータになります。つまり device=sp と userid=itempage が追加になっているということです。
レスポンシブでページを作っている場合は、これをPCスマホ時で切り替える必要があります。

なんでもいいけど、カート追加がGETで行われる楽天さん。まじでイケてないんでHTTP勉強しなおしてください!!

SPとPC切り替え

User-Agentなどで判定すればOKです。といってもわからない方もいらっしゃるとおもうので、ページは重たくなりますが、判定ライブラリを使った場合の例示も書いておきます。

HTMLに自作JSファイルをの前に

<script src="https://cdnjs.cloudflare.com/ajax/libs/mobile-detect/1.4.3/mobile-detect.js"></script>

を呼び出します。ライブラリの詳細はこちらをごらんください。

JSファイルは以下のようにします。


///パラメータ
var params = {
      'shopid': ショップID(整数値),
      'units': 個数(整数値),
      'itemid': 商品番号(整数値),
      'dbasket_choice_select[]': 'オプション文字列',
      'dbasket_choice_select[]': 'オプション文字列2'
  };

///モバイル判定
var md = new MobileDetect(window.navigator.userAgent);
if( md.mobile() != null ){
  params['params'] = 'sp';
  params['userid'] = 'itempage';
}

///ajax実行
$.ajax({
  url: 'http://direct.step.rakuten.co.jp/rms/mall/cartAdd/',
  type: 'get',
  dataType: 'jsonp',
  data: params
})
.then(function(data){
  if( data.resultCode == '0'){
    alert('OK');
  }
  else{
    alert( 'error!: ' + data.resultMessage );
  }
});

以前の記事で書いていないことがありました。カゴへ移動

処理完了時にカートへ移動したいということがあると思いますので、そちらをいれたバージョンも書いておきます。


///パラメータ
var params = {
      'shopid': ショップID(整数値),
      'units': 個数(整数値),
      'itemid': 商品番号(整数値),
      'dbasket_choice_select[]': 'オプション文字列',
      'dbasket_choice_select[]': 'オプション文字列2'
  };

///モバイル判定
var md = new MobileDetect(window.navigator.userAgent);
if( md.mobile() != null ){
  params['params'] = 'sp';
  params['userid'] = 'itempage';
}

///ajax実行
$.ajax({
  url: 'http://direct.step.rakuten.co.jp/rms/mall/cartAdd/',
  type: 'get',
  dataType: 'jsonp',
  data: params
})
.then(function(data){
  if( data.resultCode == '0'){
     location.href = 'https://ts.basket.step.rakuten.co.jp/rms/mall/bs/cartall/?shop_bid=ここにショップID';
  }
  else{
    alert( 'error!: ' + data.resultMessage );
  }
});

location.hrefを使ってショップIDを指定したカートURLにページ移動させることでカートへ遷移します。

動作参考をば

そういえばこれまえ参考動作を作っていませんでしたね。動作させてみましょう。



辛いものをカゴにいれる

↑ここをクリックするととっても辛い商品がカゴにはいるよ!!!

で何をしているかというと

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mobile-detect/1.4.3/mobile-detect.js"></script>
<script>
$(document).ready(function(){
    var md = new MobileDetect(window.navigator.userAgent);
    
    $('.btn-send-rakuten').on(
        'click',
        function(eve){
            setProduct(0);
        }
    );
    
    var productList = [
        { 'shop_bid': '205607', 'item_id': '10002002', 'option': 'とってもからいよ!' },
        { 'shop_bid': '205607', 'item_id': '10002166', 'option': 'こっちもすごいよ' },
    ];
    
    function setProduct( current ){
        var targetProduct = productList[ current ];
        var params = {
            'shopid': targetProduct['shop_bid'],
            'units': 1,
            'itemid': targetProduct['item_id'],
            'dbasket_choice_select[]': targetProduct['option']
        };
        if( md.mobile() != null ){
          params['params'] = 'sp';
          params['userid'] = 'itempage';
        }

        $.ajax({
          url: 'http://direct.step.rakuten.co.jp/rms/mall/cartAdd/',
          type: 'get',
          dataType: 'jsonp',
          data: params
        })
        .then(function(data){
             current ++;
             if( current >= productList.length ){
               location.href = 'https://ts.basket.step.rakuten.co.jp/rms/mall/bs/cartall/?shop_bid='+productList[0]['shop_bid'];
             }
             else {
                setProduct( current );
             }
        });
    }

});
</script>
</pre>
<a href="javascript:void(0);" class="button is-danger is-fullwidth btn-send-rakuten">辛いものをカゴにいれる</a>

こんなコードです。

  1. カートに入れたい商品を配列で準備
  2. カートに入れたい商品をループ(ここで関数自体を回帰呼び出し)してAjaxコール
  3. 全商品はいったらカートにジャンプ

です。JSの書き方としては、あえて古い書き方でわかりやすくしているので、JSプログラマ諸氏はご勘弁を。

1フォームで楽天で複数商品をカートに入れる方法は他にもありますが、動的にPC/SP共通でカートに入れられるというのは、サイトの表現上役に立つことが多いかと思います。

現場からは以上です。

さくらインターネットのMySQLが5.7になってORDER BYでこまったとき

古いWebアプリケーションフレームワークをつかい
MySQL5.6までのMySQLデータベースをつかったアプリケーションを
MySQL5.7のサーバーに移転するにあたり

ORDER BY clause is not in SELECT list, references column...

というようなエラーが出た場合、MySQL5.7より「ONLY_FULL_GROUP_BY」がオンになっていることが原因だ。

このあたりは、すでに多くの先駆者が設定について書いてくださっている。

など。大変ありがたい情報だ。

しかし、さくらインターネットでは権限がない

さくらインターネットなど共有サーバーでは、これらの設定については、
権限付与がなく設定ができない。

さてどうしたものか。。。

そのセッションのみで、(MySQL Workbenchをつかっている経験から)SQLMODEの変更ができるはず。
ということで

SET SESSION sql_mode = 'NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';

毎回接続時に実行できればOKなはず。と考察して、調べた結果問題なしでした。
各フレームワークや、自作のシステムの場合も、
mysqli_connect関数の直後や、new PDO()直後あたりで毎回実行するようにすればOKです。

WordPressでタイトルタグをテンプレートファイル側から指定

今日はかりゆしウェアを着ている伊藤です。どうもどうも
2018年日本は災害レベルで暑いですね。沖縄県はすずしいですよ。

さて最近連日WordPressの話題を書いています。
今日はWPのタイトルタグをテンプレート側から指定できるといいよね~ってはなしをします。

おともだちのKさんの記事を参考にして書いていきます。

前提条件

  • 包括的に全部を書き換えるのではなく、必要なタイミングでテンプレートごとに指定したい
  • テンプレートファイルからは、get_header();の直前に関数で指定したい

たぶんあるあるだと思います。SEO考えるとね。結構いじりたいところですからね。

document_title_partsフィルターを利用する

Kさんの記事によると、document_title_partsに紐づけした関数に、引数としてタイトル構造を持つ配列が渡されて、それを編集してreturnすればいいぜ。ワイルドだろー?って書いてあるます。なるほど。しかし僕のやろうとしていることだと、必要なタイミングで呼びたいという前提と、テンプレート側から指定したという条件が入るので、そのまんまは使えません。
そこで登場するのがクロージャーです。

クロージャーを使って実装

もうめんどいからソースを晒します。functions.phpにでも書いてください。

/**
 * タイトル変更
 * @param string $title
 * @param string $tagLine
 * @param string $siteTitle
 */
function setTitle( $title, $tagLine = null, $sitTitlte = null ){
    add_filter(
        'doment_title_parts',
        function( $return ) use ( $title, $tagLine, $sitTitlte ) {
            $return['title'] = $title;
            if(!is_null($tagLine)){
                $return['tagline'] = $tagLine;
            }
            if(!is_null($sitTitlte)){
                $return['site'] = $sitTitlte;
            }
            return $return;
        }
    );
}

こんかんじ。で、テンプレからは

setTitle( 'たいとるだよーん' );
get_header();

もう少しちゃんとするならクラスにして静的メッドにするのが筋でしょうねー。
クロージャーをつかっています。あんまりWPでは使われないかとおもいます。

意外に使うTipsになるのでは?
現場からは以上です。

WordPressにORMを組み込む

どうもどうも。2年近く連絡をしていなかったら育ての親である叔父が心配してきた伊藤です。

さて、WordPressにはデータベースを操作する機能というのは一応揃っています。
wpdbとかそのへんですね。ただ、基本的にはSQLを書くか、あるいは、WPのもともとのテーブル構造に対するクエリを前提としていることが多く、遠回りだったりとかしますわね。できればActiveRecordやORMがはいっているといいなーと思うわけです。

特に、従前のシステムがあって、WPでリニューアルをかけたい。というときは、従前のシステムのテーブルはそのままにリニューアルをしたいということは、多々あるわけです。

まさにそんな事を考えていたので、いっちょWordPressにORMを入れてみた。というはなしをします。

PHPのActiveRecord ORMライブラリ

PHPのActiveRecordやORMの単体ライブラリというのが、実はそもそもあまりないです。最近のPHPのフルスタックWebアプリケーションの場合、Doctrineがベースに利用されていたりとかするわけで、車輪の再開発を嫌うのか、あまりないのです。

僕の場合は、PHP5.3系の頃は、php-activerecordをよく使っていましたが、ちょっと古くなったのと、リレーションのリレーションが追えないなど、ちょっと足らないところもあります。

今回探していていい感じだなーとおもったのが、RedBeanPHPです。MySQL/MariaDBのプラグインとPHPライブラリを提供しており、かなり高速で、わかりやすいコード体系のORMになっています。しかし、MySQLのプラグインということで、共有ホスティングなどでは可搬性がさがりますので、今回は見送りました。

今回採用することにしたのがSpotORMです。Doctrine DBALをベースに作られたORMラッパーです。Relationの定義などがしやすく、僕がよく使うフレームワークYiiのModelの構造に似ているのも、採用の理由です。

ではインストール

composerでインストールします。

{
    "require": {
        "vlucas/spot2": "~2.0"
    }
}

とかですかね。composerの使い方はぐぐれ。

WPの場合は、functions.phpにコードを書く感じになるので、テンプレートディレクトリで実行すればよいかと思います。で、functions.phpの冒頭くらいに

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

とでも書いておけば良し。インストールが簡単です。

つかってみましょう

まず接続設定をします。

$cfg = new \Spot\Config();
$cfg->addConnection( 'mysql', 'mysql://ユーザー:パスワード@ホスト名/DB名' );
$spot = new \Spot\Locator($cfg);

DSNで書くとこんなかんじ。連想配列での指定ももちろん可能です。
設定はWPの接続情報を使い回すとかでもいいでしょう。

モデルを用意する

ORMなのでモデルクラスを用意します。
僕の場合は、functions.phpの並びに「Entity」フォルダを用意して、その中にモデルクラスを配置しました。オートローダーをとかを作るほどでもないので functions.phpに

$files =  glob( __DIR__ .'/../Entity/*.php') ;
if(!is_array($files)){ $files = array(); }
foreach( $files as $one ){
    require_once $one;
}

を追記します。
Entityの中身を一気にロードしちゃっています。もちろん必要時に読むというやり方のが正しいとは思います。

Modelファイルの中身は

 ['type' => 'integer', 'primary' => true, 'autoincrement' => true],
        ];
    }
}

仮にこんなかんじです。fieldsになにかないと怒られることがある?っぽいので、primaryのIDだけ定義しておきました。

呼び出しに使ってみる

最後に実際にDBにクエリしてみましょう。
呼び出しをするfuncitonの中で

function hoge(){
    global $spot;
    $postMapper = $spot->mapper('Entity\Products');
    $row = $postMapper->where([
        'item_id' => '品番'
    ])->first();
    return $row;
}

こんな感じにかきます。上記例では商品DBに品番を問い合わせる。的な事をしています。

実際組み込んでいるがWPと融合しているわけではない

ここまで読むとPHPerならおわかりになるとおもいますが、WordPressのオブジェクトの他に、DBに対して接続を一つたてることになります。したがって、メモリ効率などは悪くなります。手軽さと、パフォーマンスどちらを優先するかなどで、選択することになるとおもいます。
しかし、DB操作をして、サイトデザインをWPに合わせるなどを考えると、有用ですし、SQLを書くよりはセキュリティ面でも安心してかけますね。

現場からは以上です。

超簡単なテンプレートエンジン(条件分岐なし)ならCodeigniterからパクれ

どうもどうも。今年はそこそこブログを書いています。体調が戻ってきました。よかったよかった。決して万全ではないのですが、1年近く体調がわるかったので、まぁそのいろいろアレです。

ところで、PHPerのみなさん。
たとえば、管理画面でメールテンプレートを管理するような場合、
そのテンプレートをどう処理するか、結構悩みますよね。

同じようなかんじで、WordPressなんかで、囲みタグ型のショートコードでループをさせたいときとかもかなり悩みます。

よくある例だと、

  1. 管理画面からテンプレート編集
  2. POSTされたデータをDBに保存しつつ、テンプレートエンジン用に、ファイルにも保存
  3. 出力時に保存されたファイルをテンプレートエンジンで処理して出力

というような手順でしょうか。
まぁ悪くはないですが、セキュリティに気をつけなければいけないとか、結構手間取りますし、ファイル出力だと手軽さがなくなります。

今回はWordPressのショートコードでどうしようかなーと思ったときの対処をメモ代わりに。

やりたいこと

前提条件です。

  • WordPressの囲みタグ型ショートコードをテンプレートとして扱いたい
  • テンプレートエンジンとして動かしたいが、PHPを書くのや、Smartyのようにファイル処理を前提としない
  • 条件分岐はとりあえずいらないがループはある

という前提条件で、具体的には

[get_product_categories category="カテゴリ名"]
<ul>
    {categories}
    <li>
        <a href="{category_url}">
           {category_name}
        </a>
    </li>
{/categories} 
</ul>
[/get_product_categories]

こんなショートコードを書いてもらってテンプレート処理をしたいわけです。
囲みタグ型のショートコードについてはみなさまが書いているので、そちらをご参考に

まぁ普通にやれば

このような条件を満たそうと思うと、難しいコードでもないので、自分で簡単なテンプレートエンジンを書いてもいいとは思うのですが、いまいち気乗りがしません。自分で書くとすれば、正規表現を書いて、ゴニョゴニョとするところではありますよね。

そこで!Codeigniterの簡易パーサークラスをぱくってきます

Codeigniterには条件分岐はないですがループに対応して、ファイル処理なしで実行できる簡易テンプレートンジンがついています。これが使えないかと思ってソースを見たところ、Codeigniter本体にはほとんど依存していないことがわかり、少しの書き換えで対応ができることがわかりました。

四の五の言わずにコードを晒せ

はい。

<?php
class CI_Parser {

    /**
     * Left delimiter character for pseudo vars
     *
     * @var string
     */
    public $l_delim = '{';

    /**
     * Right delimiter character for pseudo vars
     *
     * @var string
     */
    public $r_delim = '}';


    // --------------------------------------------------------------------

    /**
     * Class constructor
     *
     * @return void
     */
    public function __construct(){
    }

    // --------------------------------------------------------------------

    /**
     * Parse a template
     *
     * @param  string
     * @param  array
     * @param  bool
     * @return string
     */
    public function parse( $template, $data, $return = FALSE ){

        return $this->_parse($template, $data, $return);
    }

    // --------------------------------------------------------------------

    /**
     * Parse a template
     *
     * Parses pseudo-variables contained in the specified template,
     * replacing them with the data in the second param
     *
     * @param  string
     * @param  array
     * @param  bool
     * @return string
     */
    protected function _parse( $template, $data )
    {
        if ($template === '')
        {
            return FALSE;
        }

        $replace = array();
        foreach ($data as $key => $val)
        {
            $replace = array_merge(
                $replace,
                is_array($val)
                    ? $this->_parse_pair($key, $val, $template)
                    : $this->_parse_single($key, (string) $val, $template)
            );
        }

        unset($data);
        $template = strtr($template, $replace);

        return $template;
    }

    // --------------------------------------------------------------------

    /**
     * Set the left/right variable delimiters
     *
     * @param  string
     * @param  string
     * @return void
     */
    public function set_delimiters($l = '{', $r = '}')
    {
        $this->l_delim = $l;
        $this->r_delim = $r;
    }

    // --------------------------------------------------------------------

    /**
     * Parse a single key/value
     *
     * @param  string
     * @param  string
     * @param  string
     * @return string
     */
    protected function _parse_single($key, $val, $string)
    {
        return array($this->l_delim.$key.$this->r_delim => (string) $val);
    }

    // --------------------------------------------------------------------

    /**
     * Parse a tag pair
     *
     * Parses tag pairs: {some_tag} string... {/some_tag}
     *
     * @param  string
     * @param  array
     * @param  string
     * @return string
     */
    protected function _parse_pair($variable, $data, $string)
    {
        $replace = array();
        preg_match_all(
            '#'.preg_quote($this->l_delim.$variable.$this->r_delim).'(.+?)'.preg_quote($this->l_delim.'/'.$variable.$this->r_delim).'#s',
            $string,
            $matches,
            PREG_SET_ORDER
        );

        foreach ($matches as $match)
        {
            $str = '';
            foreach ($data as $row)
            {
                $temp = array();
                foreach ($row as $key => $val)
                {
                    if (is_array($val))
                    {
                        $pair = $this->_parse_pair($key, $val, $match[1]);
                        if ( ! empty($pair))
                        {
                            $temp = array_merge($temp, $pair);
                        }

                        continue;
                    }

                    $temp[$this->l_delim.$key.$this->r_delim] = $val;
                }

                $str .= strtr($match[1], $temp);
            }

            $replace[$match[0]] = $str;
        }

        return $replace;
    }

}

Codeigniterのコントローラーオブジェクトに依存しているところなどを削って純粋にclassとして動くようになっています。

じゃWPで動かしてみよう

functions.phpあたりで、上記ファイルをrequireして

add_shortcode( 'get_product_categories', 'get_product_categories');

functionget_product_categories( $atts, $content = null ){

    //このへんで適当に引数処理してね    
        
    $parser = new CI_Parser();
    $args = array( ここに適当に条件かいてね );
    $the_query = new WP_Query($args);
    if (!$the_query->have_posts()) {
        return '';
    }
    
    $data = array();
    $data['categories'] = array();//ショートコードのループタグと名前を合わせる
    while ( $the_query->have_posts() ) {
      $the_query->the_post();
      $set = array(
        'category_name' => get_the_title(),
        'category_url' => get_the_permalink(),
      );
      $data['categories'][] = $set;
    }
    wp_reset_postdata();
    
    
    $content = $parser->parse( $content, $data );//最後にパースする。
    return $content;
}

とでもしておけば、簡単ね。

多分改造でifくらいはいける

上記の通りCodeigniterのパーサーはシンプルにできてます。
たぶん改造すればifくらいは余裕でかけると思います。
が、他方、毎回実行時にパーサーが走るわけなので、決して軽くはありません。
使い所見極めていきましょう。

なお、Codeigniter3.xはMITライセンスなのでOKですが、2.xまでのコードを使う場合は独自ライセンスなので、扱いに気をつけてください。

現場からは以上です。

楽天PayオーダーAPIをPHPからコールしてみた

どうもこんにちは伊藤です。きよっちです。半年ぶりの更新となりました。真面目に書けよ!と思われるかもしれませんが、まったくそのとおりですね。ちょっといろいろ思うところがございまして、沈んでましたが、それはまたおいおい。

はい。ところで。楽天さんの動きが激しいですね。楽天Payへの移行などなど。やらなくてはいけないことがたくさん。ちょうど今年2018年はヤマト運輸さんの配送料金大改革などなどもありましたし、Instagram上で通販が可能になったりしましたし、環境は大きく揺れ動いています。日本のECがはじまって以来、最も大きな変革期に来ているのかもしれません。ECを主戦場にしている業者さんは、自社の立ち位置、目指す場所、戦い方(戦わないという選択肢も含めて)などを明確にできる機会かもしれません。

ちょっと話がズレてしまいましたが、さて、楽天は改革を迎えているわけで、個人情報の扱い厳格化、決済方法の統一化などがあり、なるべく多くの受注操作をRMS上で行わなければならないようになってきています。

そんなこともあり、最近では、自社開発DBとの連携のために、RMS Web Service APIを利用しなければならないということもかなり増えてきました。今日はそのお話です。

RMSのAPI

まず、もってですね、楽天のRMSのAPIというのは、みなさんご存知でしょうか。2012年ごろからスタートしているRMS用のAPIです。それより以前は、公開情報(つまり商品情報ですね)を取得して、楽天外のwebサイトで活用できるようになっていたわけですが、RMS APIができてからは、RMSを楽天のシステム外で利用できるようになっています。(もちろん自社運営の楽天サイトの情報しかとることはできません) 経緯はこのあたりにくわしいです。

このRMSのAPIというやつがですね、わたしたちwebをメインにしているエンジニア・プログラマには、あまり馴染みのない「SOAP」という規格のAPIを採用しています。こいつがなかなか厄介で、webで多く使われているPHPでは、コードが冗長になりがちなのです。

そのコードについてはこちらのサイトにてサンプルが書かれていますので、参考になります。わたしも最初は参考にしました。

楽天PayOrderのAPI

上記のように、まぁぶっちゃけて言うと大変に分かりづらいSOAPを仕様したRMS APIなんですが、一回実装してしまえば、そうそう変更するものでもないので良しとしていました。

ここからが本題です。

ところがですね、楽天Pay移行により注文情報取得APIを変えなければならなくなりました。2018年以降、楽天各店は決済方法統一のため楽天Payに強制移行となりますので、既存APIを利用していた事業者さんは、いずれ、のりかえが必要ということです。 楽天Payへの移行については、こちらの記事がよくまとめられています

しかし、文句ばかりも言っていられませんので、楽天へ出店しているクライアントさんのために、API改造に着手しました。いつもそうなんですが、楽天さんのエンジニア向けドキュメントは、ぜんぜんまとまってなくて、どこにいけばわかるのかがわからない。たどり着いたとしても、実は全体像はそこだけではわからない…など前途多難なのです。

とりあえずドキュメントにたどり着いたので、従前のOrderAPIを書き換えてみる。

…あれ…なんか変だ…動かない…というよりは、楽天から返ってくる書式が変だ…
…ん???
…あれ??????
(ここまで1時間)

ようやく気付いたのですが、楽天PayOrderAPIだけが、基底となっている技術そのものを変えてきやがったのです!!!もう「やがった」といいますが許してください!

よく読めと言われれば、それまでですが、楽天PayOrderAPIだけは「JSONベースのSOAP」なのです。APIさわったことのあるエンジニアならわかるとおもいますが「お前何いってんだよ!」ってなりました。「SOAP」はもともとXML通信インターフェイスだろうよ!!!リクエストもレスポンスもJSONでやるのに、技術そのものはSOAPなのです。この矛盾に気づくのに時間がかかりました。(ここから想像するに、おそらく楽天PayのAPIの基底フレームワークはIBMのCICSだと思われます。もう少し標準的な仕様にしてくれませんかね???)

とっととコードを出せ

はい。。。ごめんなさい。。。取り乱しました。

class RMS {
    
    public static $serviceSecret;
    public static $licenseKey;
    
    // ----------------------------------------------------
    /**
     * オーダー取得
     */
    public static function searchOrder(){
        
        ////認証キーを作成
        // oAuthのが1289倍楽なんですけど!!!
        $authkey = "ESA " . base64_encode( self::$serviceSecret . ':' . self::$licenseKey );
        
        ////検索日付
        // そんなもん必須にせずに良きに計らえや!!!!
        // しかもいちいちGMTに変換しなきゃあかんのかい!!!それも良きに計らえよ!!!!
        // 第二引数で日本時間指定できるようにしておいた
        $beginDate = gmdate( 'Y-m-d\TH:i:s+0900', strtotime('2018-07-05 00:00:00'));
        $endDate = gmdate( 'Y-m-d\TH:i:s+0900', strtotime('2018-07-29 00:00:00'));
        
        //// なぜか楽天ペイAPIだけ しかも形の上ではSOAPなのに JSONでリクエスト…
        // 統一してくれんかな!!!ライブラリ書くのめんどくさいんだけど!!!!
        $requestJson = json_encode([
            'dateType' => 1,//期間検索種別
            'startDatetime' => $beginDate,//検索対象期間先頭日時(普通startじゃなくてbeginじゃねぇか??)
            'endDatetime' => $endDate,//検索対象エンド点
            'orderProgressList'=> [ 100, 200, 300, 400 ],//取得したいオーダーステータス
        ]);
        
        
        //// リクエストヘッダ作成
        // うーん なんかこの認証いまいちだなー・・・
        $header = [
            'Authorization: '.$authkey,
            'Content-Type: application/json; charset=utf-8',
        ];
        
        
        //// SOAPリクエスト
        // この辺はREST APIでのPOSTと一緒。ただし、レスポンスコードで結果コードをとるので
        // レスポンスヘッダについては厳重目にとっておくべき。
        $url = 'https://api.rms.rakuten.co.jp/es/2.0/order/searchOrder/';
        $curl = curl_init($url);
        curl_setopt( $curl, CURLOPT_POST, true );
        curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $curl, CURLOPT_HTTPHEADER, $header );
        curl_setopt( $curl, CURLOPT_POSTFIELDS, $requestJson );
        curl_setopt( $curl, CURLOPT_HEADER, true);//これを指定するとレスポンスにヘッダがついてくる
        
        // curlセッションを実行 ヘッダが付いているので分解
        $response = curl_exec( $curl );
        
        // ステータスコード取得
        $statusCode = curl_getinfo( $curl, CURLINFO_HTTP_CODE );
        
        // ヘッダサイズを取得して、その分を切り取ってしまう方策
        // もう少しいい方法があるかもしれない??
        $headerSize = curl_getinfo( $curl, CURLINFO_HEADER_SIZE );
        $requestHeader = substr( $response, 0, $headerSize);
        $requestBody = substr( $response, $headerSize );//ここに結果JSONがかえる
        
        
        //// jsonをオブジェクトにー
        $return = json_decode( $requestBody );
        
        curl_close( $curl );
               return $return;
        
    }
    
    // ----------------------------------------------------
    
}

ざっくりとclassにしました。モダンなコードではないですし、何かが漏れていますが気にしないでください。
引数でパラメータを指定できるようにするなどの改造は必要だとおもいますし、いくつかのエンドポイントを実装するにあたってはPOST部分を独立メソッドにすべきでしょう。そこは、皆さんのセンス。

このクラスをロードしておいて

<?php
RMS::$licenseKey = 'ライセンスキー';
RMS::$serviceSecret = 'シークレットキー';
$res = RMS::searchOrder();

とかでだいたい勝てます。

以上、現場からでした。
「室井さん。どうして現場に血が流れるんだ!」(←古い)

楽天の注文フォームに無茶させるver.2

6年ほど前に楽天の注文フォームに無茶をさせるという記事をかきました。

なんかちょいちょい利用されているようでして、、、
こんな記事でも役に立つのかっていうはなしなんで、
ちゃんと最新版をつくっておきます。

何をトチくるったかajaxでカートに入れられるようになりました

以前は、フォームを作成しなければ、
カートに入れることができなかったのですが、
現在は楽天が、ajax経由で、カートにいれることができる仕組みを用意しました。
なかなかトチ狂っています。
最近は遷移なしでカートに入れるUIが流行っているので、
そのために準備したのでしょう。

仕様

以前の方法と同じく、ショップIDと商品番号(RAC番号)を特定しておく必要があります。
詳しくは以前の記事を。

パラメータは、

{
  'shopid': ショップID(整数値),
  'units': 個数(整数値),
  'itemid': 商品番号(整数値),
  'dbasket_choice_select[]': 'オプション文字列',
  'dbasket_choice_select[]': 'オプション文字列2',//オプション文字列は複数可能
}

です。(上記はjavascriptのオブジェクトで表現しています)

これをajaxするだけです。
宛先は
http://direct.step.rakuten.co.jp/rms/mall/cartAdd/
です。

jQuery前提で、書くなら、

$.ajax({
  url: 'http://direct.step.rakuten.co.jp/rms/mall/cartAdd/',
  type: 'get',
  dataType: 'jsonp',
  data: {
      'shopid': ショップID(整数値),
      'units': 個数(整数値),
      'itemid': 商品番号(整数値),
      'dbasket_choice_select[]': 'オプション文字列',
      'dbasket_choice_select[]': 'オプション文字列2'
})
.then(function(data){
    //ここで結果を解析
});

という感じです。

ポイントは「json」ではなく「jsonp」にするところ。まぁこれは並のフロントプログラマなら意味がわかると思うので、割愛します。

えっと。。。。なんで「GET」で動いちゃうんですかね!?楽天さん!!!
HTTPの定義では、GETはデータを取得する目的で動くメソッドのはず、、、、
何かしら、データやシステム動作に影響を与える処理の場合は「POST」にしましょうね。。
楽天さん。まぁそんなこといいだしたら、CSSの仕様とか色々アレですかね!!!

というDISはここまでにして、、、、

さて返り値は

{
   addItemMessage: true,
   cartURL: null,
   dialogTitle: "買い物かごに追加されました",
   includedPostage: false,
   includedTax: false,
   itemName: null,
   itemPageUrl: null,
   price: null,
   resultCode: "0",
   resultMessage: ""
}

こんな感じの結果がかえってきます。

なんとなく見ると分かるのですが「dialogTitle」とあるように
本来は、楽天が公式的にサイト内でダイアログを表示するために使うAPIのようです。

で!またこのjsonが何だかなな仕様でして
resultCodeが「0」だと成功です(しかもそして文字列です)
そんでエラーがあればresultMessageに値が入ります。

したがって、resultCodeとresultMessageを見れば、結果がわかります。

$.ajax({
  url: 'http://direct.step.rakuten.co.jp/rms/mall/cartAdd/',
  type: 'get',
  dataType: 'jsonp',
  data: {
      'shopid': ショップID(整数値),
      'units': 個数(整数値),
      'itemid': 商品番号(整数値),
      'dbasket_choice_select[]': 'オプション文字列',
      'dbasket_choice_select[]': 'オプション文字列2'
  }
})
.then(function(data){
  if( data.resultCode == '0'){
    alert('OK');
  }
  else{
    alert( 'error!: ' + data.resultMessage );
  }
});

みたいな感じですかね。あ!そもそもHTTPのエラーが帰った場合の処理は上記コードには入っていませんから注意してください。

実践!!

仮に、楽天でたまたま見つけた
https://item.rakuten.co.jp/kadenshop/4111-snw-1567/?s-id=top_normal_rk_hashist_realtime
こちらのページの商品をカートに入れてみましょう。

$.ajax({
  url: 'http://direct.step.rakuten.co.jp/rms/mall/cartAdd/',
  type: 'get',
  dataType: 'jsonp',
  data: {
      'shopid': 244202,
      'units': 1,
      'itemid': 10248876,
      'dbasket_choice_select[]': 'オプション文字列',
      'dbasket_choice_select[]': 'オプション文字列2'
  }
})
.then(function(data){
  if( data.resultCode == '0'){
    alert('OK');
  }
  else{
    alert( 'error!: ' + data.resultMessage );
  }
});

ですねー!

ショップIDと商品番号は、商品ページのカゴに入れるボタン周辺に
input type=hidden

「shop_bid」と「item_id」という名前で表示されているので、
ソースから引っ張ってこれます。

つまり他人の店でも、カートに入れる処理ができます。
まぁ何の意味もないですが。。。

で、先程の説明どおり、なんでか知らないですけど、
これGETで通っちゃうので、

http://direct.step.rakuten.co.jp/rms/mall/cartAdd/?shopid=244202&units=1&itemid=10248876&dbasket_choice_select[]=オプション文字列&dbasket_choice_select[]=オプション文字列2

このURLをブラウザ直打ちでも動作確認できます。。。おいおい(´・ω・`)

どうですか?

無茶をさせているようですが、結構使える技術です。
見積結果として複数の商品を一気にカートに入れたい場合などにかなり重宝をします。
以前の記事の頃ですと、formをsubmitするなどしなければならず
セキュリティソフトウェアがブロックするなどの問題がありそうでしたが、
これならその危険性はないです。

クルーズ旅行に出てみた!出発まで編


2018年一発目のネタづくりとして、というのはアレですけども、生まれて初めて船での長旅に出てみようと思いました。7泊8日の船の旅、名古屋を出て、那覇、台湾の基隆、宮古島を経て東京へいく旅です。

いまはその旅の最後の夜。船の中でこの記事を書いています。

その感想を書いてみましょう。

なんで乗ろうと思ったのか

クルーズの旅に出ようと思ったのは、テレビ朝日で放送されている『陸海空 地球征服するなんて』という番組で、大西洋を渡るクルーズ船にのって、さまざまな人に会ってみようという企画を見たのがきっかけです。

船旅というのは、時間もかかり不便で、金持ちの道楽か何かだろう。ぶっちゃけるとそんなイメージを持っていました。が、しかし、テレビをみて、こんなにたくさんのひとに会えるのは楽しいかもしれない。そして、これだけ不便な生活を一回やってみよう。そんな風に思ったわけです。

なんとなーく調べてみると、クルーズ旅行は、いくつかのクラスがあり、外国船籍のカジュアルクラスの船ならば、2人1室での旅なら1泊10,000円程度、1人でも1泊18,000円程度で、食事つきの旅に出ることができるということを知りました。これは現実的だ。というわけで申し込んだわけです。

しかし不安は大きい

あるとき旅行代理店から電話がかかり「旅行代が安くなった」と連絡をうけました。そう言われても数千円だと思いますよね。ふつう。「45,000円のお値引きでございます」。えっ??どういうこと??何の割引???特に説明はありませんでしたが、先に振り込んでおいた旅行代金から45,000円が返ってきました。さすがにこの価格となると、安全は大丈夫なのかと不安が出てきます。

2018年に入り、いよいよ出発が近づくと、いろいろ不安も出てきました。さすが、ともいうべきなのか、海外のクルーズ専門会社、しかもカジュアルクラス。乗船券が届かない(笑)代理店に聞いても、発行してくれないんですよ(泣)みたいな反応で、おいおいと思いました。が、しかし、船に乗った後に、クルーズ旅行の慣れているひとによると、よくある話だそうです。ははは

しかし、乗船券が届いてみると、まさかの普通紙にレーザープリンタで印刷された乗船券が届くという、なんとも「カジュアル」な展開。それどころか、荷物を預けるためのタグですら普通紙。びっくりびっくりびっくり。

webのお仕事をしている身からすれば、船内でのインターネット接続については気になるところ。代理店に聞いたところ「あくまでも目安なんですが従量制で250MBあたり10USDです」との返答。ちょっと待ってくださいよ!!従量制なのはいいですけど、価格が目安ってどいうことですか!!?? なんでも、乗る船によって異なるとか…は。。。は。。。

言い知れぬ不安だけが大きくなって来ましたw そもそも、クルーズ旅行は「わざわざ行く」と決心しないかぎりは、現代の長い人生の中でも、一生行かないタイプの旅行です。ほかの旅行とは、準備がちがいますから、仕方ないですね。。。

しかし、こういった不安は、ほとんどが余計な不安に終わるわけですが、後日この不安は、実は別の悪いことへの予感だったのです。後日おはなししましょう。

さぁ出発だ。

2018年1月20日。いよいよ出発の日を迎える。あらかじめ届いた案内には、「名古屋港ガーデンふ頭に来てください。12時くらいから乗れます」とだけ。。。あいかわらず不案内ですな。というかんじで、水族館いくか、花火を見に行くかでもない限りいかない、名古屋港ガーデンふ頭へ。

近づいていくと、oh!見たことのない大きな船が!これに乗るのかとおもうと、ワクワクしてきました。そこまで考えていた不安は飛びました。というか「でっかい山があるから登ってやれ!話はそれからだ!」と言う感じかもしれませんが。

乗船に関して、審査というのは、あまり厳しくはなかったです。金属探知機を通りますが、航空機ほど厳密ではないです。その代わりなのかわかりませんが、パスポートのチェックは何重にもなっていました。港湾局、船会社、船に同上セキュリティの3回のチェックでした。

びっくりしたのが、パスポートは船のスタッフによって人質にとられます。航空機とは本当にいろいろちがうんですね。

船にのってみると



カジュアルクラスの船とはいえ、さすが洋上のホテルというつくりです。船は12フロア構造。わたしが泊まるのが、フロア6「ロンドンフロア」と名付けられたフロアです。部屋にはいるとシングルベッドが2つ。十分な広さの居住空間が保たれており、なかなかに快適そうです。

11フロアにはプールとジャグジーとバー、軽食エリアがあります。なんと開放的!!

ちなみに、わたしは最初迷いましたし、他の多くのお客さんも迷っていたのですが、食事は3食付で、出発日は夜だけという案内なのですが、実は、ブッフェは営業時間内であれば、いつでも好きなだけ食べられます。中途半端な時間に食事を取ることができずに船に乗った場合は、どうぞ最初ぶブッフェへいってみてください。

ゆったりするぞー!と言う感じがひしひしと湧いてきたところで、「ぶぉーーーーー」と汽笛が鳴り、出航となりました。

プライスレスな体験のはじまりを体感しました。マスターカード。
そんなわけで続きをどこかで書きましょうー。

出発まで編のまとめ

  • カジュアルクラス外国船籍のクルーズなら、1人でも1泊18,000円で食事付き。そして、謎の大幅割引がつく
  • わりとざっくりとした案内だが、そういうものだと思うべき
  • 価格の割にはしっかりした居住空間
  • 出発からして、プライスレスな体験
  • 船のセキュリティは航空機と違う。高飛びに使えるかも。

自分がいる業界以外でセミナー登壇した話

2017年

毎年セミナーやらなんやらの登壇は、2から3回をこなしています。例年なら、おおよそ、自信が生業としている、webだったりとか、通信販売の世界でのセミナー登壇となることが多いわけですが、2017年は、関わりがない業界からの、セミナー依頼がいくつもありました。
そのお話を。

ちなみにその業界というのが

  • メガネ業界の勉強会
  • 某市の商工会議所
  • 某旅行会社

などですね。

自分の業界の歴史をひもとくことからやらないといけない

自分の業界向けのセミナーであれば、ひとつひとつの事柄について
「そういうものだ」という共通認識があるし、
「そういうものだ」という共通認識によって教育をされてきているので、
その共通認識のもとに、話を組み立てていけばよいですが、
業界外のセミナーとなると、ほぼすべての事柄について、
なぜそうなのか?という、そもそもの「Why」から紐解いていく必要がある。
そして、聞く側も、それを求めている。

たとえば

  • 「web」ってなによ
  • インターネットとwebの違い
  • googleの役割
  •  ホームページって?ブログって?

みたいな話だ。
webを生業としている人にとっては、ほとんど空気みたいに認識している「感覚」だが、
これを明文化しなければならないことが多い。

それってどういうこと? それって何?という質問攻めにあい、冷や汗の連続となる。
最初はとても戸惑った。

「頭の中の索引」

自分が働く業界外のセミナーでは、こんなふうに「そもそもの質問」攻めにあうので、
こういった場所で登壇するには、自分の知識量が問われる。
これは「物知り」ということではなくて、

  • あーそういえば、あのセミナーでこういうこと言っていたな
  • そういえば、どっかで、この情報見たな

という「自分の頭の中の索引」が重要。

「引き出し」とも言えるが、
ぶっちゃけると、細かく調べることは後でもできちゃう。
必要でなければ覚えることもないでしょう。

他業界でのセミナー登壇というのは、
知識を固定化する上でもとても重要な経験だと断言できる。

この記事をご覧になっている みなさんは上記の質問に明確に回答できますか?
おそらく多くの方は、それとなく回答する結果になるのではと思う。

当たり前は当たり前じゃない

よく言われることですが、自分の常識は他人にとって非常識。
自分の業界では当たり前でも、他業界で当たり前でないことは大量にある。
これは「マーケットギャップ」「市場差」と捉えるとおもしろい。
他業界の方のお困りごとに対して、
「普通に考えたらこうだ」という回答をするだけで、
ゲスい言い方になるが、なかなかにヒーロー扱いをうける。

前述のメガネ業界のセミナーでは、
webを使った集客の基本の、さらに基本の部分の内容だった。
内容としては、
「しっかりターゲッティングして、顧客行動を見ましょうね」
という話をざっくりとした内容。

その中で質問されたことに対する回答として感謝されたことは
「補聴器とサングラスは商材としてかけ離れている。サイトを分けることも検討すべき」
と指摘したこと。

補聴器とサングラスは、webを作る業界にいれば、
入り口も目的も顧客行動も違うのはすぐにでもわかることだが、
彼らにとっては、この2つは
「眼鏡屋に来る客が買うもの」
としてまとめられてしまっていた。
「そういうものだ」という常識にとらわれていた。
だが、指摘をした上で
「想定する顧客の姿は違うものですよね?」
と説明したら
「すげぇ!!!」ってなったわけだ。

マーケットギャップを利用すればヒーローになることはできる。
どうせ、登壇するなら、そのほうが気持ちよくできる。

結果的にいくつか仕事も産んでいる。

そんなセミナーどっから話がくるの?

こんな話を先日友人にしていたら、そんな話どっからくるのさ?といわれた。
別にセミナー登壇の仕事は労力に見合っていない報酬になることも多く
正直やりたくない仕事のひとつだ。
したがって「ほしい」などとはひとこともいっていない。

ただ、ひとついえるのは、いずれも元々のクライアントさんからの紹介だ。
クライアントさんに対して、普段から新しい技術や知識や成果をあたえることが結果的に
他業界での登壇の誘いとなった。
多くのビジネスが、取引先に足しいて、これらの価値提供をすることに終始するといえる。
「商いとは他己中心だ」と、よく言われることだが、まさにそれで、
他己が結果的に自己になるっていうやつかもしれない。

というわけで言えるのは

自分の知識が強烈なインパクトを与える
自分が働く業界ではない別の業界で登壇するのは
いろいろおいしいよってはなし。

ボン・ヴォヤージュ!

CentOS7+PleskOnyxな環境でのはまりごと

CentOS7 + PleskOnyxな環境をはじめて借りてハマったことをメモ

イニシャルではApacheモジュール版PHPがうごいていない

Pleskではすこし前のバージョンから

  • PHPの動作モード
  • PHPのバージョンを選択

できるようになっていますが、PleskOnyxでは、PHPの動作モードが

  • FAST CGI
  • FMP

のみになっており、あれ?Apacheモジュールは???となりました。
サーバーを借りたクララオンラインさんに、サポートをおねがいしたところ
http://faq.clara.jp/index.php?solution_id=1350
をご紹介くださいました。なーるほど。Pleskはもともといろーんなものがモジュール化していますが、
mod_php自体もモジュールなんですね。


CPANモジュールが共通ライブラリディレクトリにインストールできない

sshでログインした際に自動的に環境に

  • PERL5LIB=/ユーザーディレクトリ/perl5/lib/perl5:
  • PERL_MB_OPT=–install_base /ユーザーディレクトリ/perl5
  • PERL_LOCAL_LIB_ROOT=:/ユーザーディレクトリ/perl5
  • PERL_MM_OPT=INSTALL_BASE=/ユーザーディレクトリ/perl5

がセットされる。
これに気づかずに3時間なやんだ。とっとと
perl -V
してれば気づいたのに。。。

これら環境変数を削除した上で、CPANインストールすればよろし。


cgi-binディレクトリが有効になっていない

まぁPerl/CGIとかRuby/CGIとかやんなやって話ですが、昔の枯れた技術というのはとても有用でしてね…
で、イニシャル状態では、公開ディレクトリにcgi-binディレクトリがあって、
そっちが有効になっているんです。昔良くあった構成

/ユーザーディレクトリ/cgi-bin/
/ユーザーディレクトリ/httpdocs/

って言う構成がとれないわけです。
ので、ふるいサイトを移行するとちょっとめんどい。

/ユーザーディレクトリ/httpdocs/cgi-bin/

の中に格納すればいいわけですが。


1個1個はどうってことない話ですけど
勘がない環境だとハマりますね。って言うメモです。

Older posts