まさひちの道具箱

趣味の話から技術・仕事まであらゆることをたれ流していきます

Git hooksを使ってphpのバージョンアップ対応までの暫定対応をする

しばらく放置してしまって、久々の更新です。

php5.xが2018年末でEOLということで、仕事でphpのバージョンアップ対応をしているのでそこで行った対応の一部をまとめておきます。

環境

今回更新をしていく環境はざっくり以下のような感じです。

かなーりレガシーな感じの構成ですが、今まで動いてたので放置してました。

これを以下のような構成にバージョンアップしていきます。

PHP-7.1は結局2019年末にEOLなんですが、

メジャーバージョンアップを乗り越えればまあなんとかなるだろうということで、今回は7.1までにしました。

手順

昔からあるコードとか、どこで不具合があるかわからないので、以下のような手順を踏むことにしました。

  • php7ccでリスクのあるコードを検出

GitHub - sstalle/php7cc: PHP 7 Compatibility Checker

こいつを使って互換性チェックを行いました。

使い方については以下の記事を参考にしています。

PHP7の互換性チェック

php7ccはすでに開発終了していて後継のツールがあるのですが、

それらはPHP7環境でしか動作しないので環境作成が面倒なため、あえてこれを使いました。

幸いなことに、今回対応したシステムはユニットテストのコードカバレッジがそこそこ高かったので、まずはユニットが全部通ることを検証しました。

その過程でカバレッジが低い部分については追加でテストを書いて、コードカバレッジで品質を担保できるようにしました。

実際のユースケース通りに操作して問題ないかをテストしました。

ここまでくるともうほとんどバグは見つからない状況でした。

まとめ

文章ばかりになってしまいましたが・・・

PHP7のバージョンアップではすでに非推奨とされていた使い方ができなくなるなど、いい感じに移行しやすくされていると感じました。

それとユニットテストがちゃんと書いてあることの素晴らしさというか、安心感は感じました。

バージョンアップ対応のような完璧に把握することが難しい作業をするときには、テストがあると本当に便利ですね・・

Goでhttpリクエスト(エンコード編)

前回、Goでhttpリクエストを送る - まさひちの道具箱でYOLP(地図サービス)の郵便番号検索APIを叩いた記事の続きです。

無事に叩くことはできたけど、出力結果が文字化けしていてモヤっとするので、その部分を直していきます。

httpリクエストの文字エンコード

前回は単純にリクエストパラメータとURLだけ設定してAPIを叩きました。

結果は

{"ResultInfo":{"Count":1,"Total":1,"Start":1,"Status":200,"Description":"","Copyright":"","Latency":0.015},"Feature":....

のようにJSON形式で返ってきたはずです。

この時、例えば

..."Name":"\u5ddd\u53e3\u5143\u90f7"...

のようにエンコードが間違っているのが気になりますよね。

これ、 文字化け解読ツール「もじばけらった」 で解析してみると、JSONエンコードにすると正しく見えるようです。

出力する文字コードを指定するにはjson.Marshalを使ってパースしてやればいいみたいです。

構造体の定義

JSON形式にパースするにはデータを入れるための構造体の定義が必要です。でも面倒なので、さっきAPI叩いて返ってきた結果をそのまま JSON-to-Go: Convert JSON to Go instantly に突っ込んで作ってもらいます。

type YOLP struct {
    ResultInfo struct {
        Count       int     `json:"Count"`
        Total       int     `json:"Total"`
        Start       int     `json:"Start"`
        Status      int     `json:"Status"`
        Description string  `json:"Description"`
        Copyright   string  `json:"Copyright"`
        Latency     float64 `json:"Latency"`
    } `json:"ResultInfo"`
    Feature []struct {
        ID       string `json:"Id"`
        Gid      string `json:"Gid"`
        Name     string `json:"Name"`
        Geometry struct {
            Type        string `json:"Type"`
            Coordinates string `json:"Coordinates"`
        } `json:"Geometry"`
        Category    []string      `json:"Category"`
        Description string        `json:"Description"`
        Style       []interface{} `json:"Style"`
        Property    struct {
            UID        string `json:"Uid"`
            CassetteID string `json:"CassetteId"`
            Country    struct {
                Code string `json:"Code"`
                Name string `json:"Name"`
            } `json:"Country"`
            Address              string `json:"Address"`
            GovernmentCode       string `json:"GovernmentCode"`
            AddressMatchingLevel string `json:"AddressMatchingLevel"`
            PostalName           string `json:"PostalName"`
            Station              []struct {
                ID       string `json:"Id"`
                SubID    string `json:"SubId"`
                Name     string `json:"Name"`
                Railway  string `json:"Railway"`
                Exit     string `json:"Exit"`
                ExitID   string `json:"ExitId"`
                Distance string `json:"Distance"`
                Time     string `json:"Time"`
                Geometry struct {
                    Type        string `json:"Type"`
                    Coordinates string `json:"Coordinates"`
                } `json:"Geometry"`
            } `json:"Station"`
        } `json:"Property"`
    } `json:"Feature"`
}

すぐにこんな構造体を返してくれました。

typeだけ直したら完成です。そのままコードに突っ込みます。

encoding/jsonjsonをパース

まず、jsonを使うのでimportの中に

"encodig/json"

を足します。

処理は、前回のコードの最後に

var data1 YOLP // 定義した構造体変数を宣言
json.Unmarshal(body, &data1) // jsonパース
fmt.Println(data1)

を加えるだけで完成。

めでたく住所などが表示されるようになりました。

YOLPを提供しているヤフー本社の郵便番号102-8282を指定すると

....東京都千代田区紀尾井町1-3東京ガーデンテラス紀尾井町紀尾井タワー 13101 6 ヤフー 株式会社......

のように、住所が正しく取れています。

まとめ

jsonデータを受け取った時はエンコードjsonエンコードになっているので、

encoding/jsonをインポートして、Unmarshalでパースしましょう。

Goでhttpリクエストを送る

特にこれといって動機はないのですが、Go言語でバッチ処理的にAPIを叩く処理を実装してみたのでメモ。

なにもGoでやらなくてもPHPだって、Pythonだって、なんならshellだってできるのですが、 なんとなくGoってさわったことないし、やってみるか的な感覚です。 特にすごいことでもないし、他の方の記事なんかで散々書かれているのでわざわざ新しくまとめる意味もないんですが。

Go環境構築

まずは環境を整えることから。

https://golang.org/dl/から最新バージョンのGoをダウンロードします。

sudo wget https://storage.googleapis.com/golang/go1.10.linux-amd64.tar.gz

うまくいったら、解凍

sudo tar -C /usr/local -xzf go1.10.linux-amd64.tar.gz

/usr/localは別に違っててもいいです。好きなところを指定してください。

次にgoコマンドを認識できるようにPATHを通します。

$ vim ~/.bash_profile
// 以下を追記
export PATH=$PATH:/usr/local/go/bin

// 認識させる
$ source ~/.bash_profile

うまく通って入れば

$ go env

で、goの環境設定が表示されるはずです。

GOARCH="arm"
GOBIN=""
GOEXE=""
GOHOSTARCH="arm"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/user/go"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/linux_arm"
GCCGO="gccgo"
GOARM="6"
CC="gcc"
GOGCCFLAGS="-fPIC -marm -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build984502013=/tmp/go-build -gno-record-gcc-switches"
CXX="g++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"

httpリクエストの実装

適当な場所にmain.goを作って実装を書いていきます。

何か叩く先があると楽しいので、今回はYahoo!デベロッパーネットワークで公開されているYOLP(地図サービス)の郵便番号検索APIを叩いてみます。

YDNはアプリケーション名を登録するとAPIを叩くためのappidが発行されるので、簡単に叩けます。

http.Client

Goでhttpリクエストを送信する方法はいくつかあるようですが、

一番カスタマイズできるやつを覚えておけば問題ないだろう(大は小を兼ねる的な)ということで、http.Clientを使います。

importにnet/httpを加えてください。

  • リクエストクエリの作成
request , err := http.NewRequest("GET", URL, nil)

のようにするとGETリクエスト用のhttp.Requestとエラーを返します。 第三引数はパラメータですが、自分の場合は

// valuesにリクエストパラメータを設定
values := url.Values{}
values.Add("appid", string(appid)) // YDNのappid
values.Add("query", "xxx-yyyy") // 検索する郵便番号
values.Add("output", "json") // レスポンスの形式:今回はJSON

reqest.URL.RawQuery = values.Encode()

として、後から設定する派です。

client := new(http.Client)

でhttp.Clientを作ってから

resp, _ := client.Do(request)
defer resp.Body.Close()

でhttpリクエストを送信します。 resp.Bodyにレスポンスの実体があるので

body, err := ioutil.ReadAll(resp.Body)

はい、レスポンス取得できました。

エンコードがおかしかったりして文字化けしてたりするので、次回はそこらへんを直していきましょうか。

まとめ

今回作ったものの全容はこんな感じ

package main

import (
    "fmt"
    "net/http"
    "net/url"
    "io/ioutil"
)

func main() {
    // サンプルとしてYOLPの郵便番号検索APIを叩く
    values := url.Values{}
    values.Add("appid", <appid>)
    values.Add("query", <郵便番号>)
    values.Add("output", "json")

 url := "https://map.yahooapis.jp/search/zip/V1/zipCodeSearch"

    // http.Getでリクエストを作る
    request, err := http.NewRequest("GET",url,nil)
    if err != nil {
        fmt.Println(err)
        return
    }
    request.URL.RawQuery = values.Encode()

    // リクエスト送信
    client := new(http.Client)
    resp, _ := client.Do(request)
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))

ラズパイのmysqlが再起動できない

ラズパイで使っていたmysqlが、ある日突然起動しなくなりました。 昨日まで普通に動いてたやんけー!!って思いながら原因調査。 なんだか結構ハマってしまったので、メモしておきます。

問題発生

久々にラズパイで

$ sudo apt-get update

を叩いたところ、、mysqlでエラーが発生。

試しにリスタートしてみる。

$ sudo systemctl restart mysql.service

結果は・・・

Job for mysql.service failed. See 'systemctl status mysql.service' and 'journalctl -xn' for details.

$ sudo systemctl status mysql.service
● mysql.service - LSB: Start and stop the mysql database server daemon
   Loaded: loaded (/etc/init.d/mysql)
   Active: failed (Result: exit-code) since 1min 24s ago
  Process: 20927 ExecStart=/etc/init.d/mysql start (code=exited, status=1/FAILURE)

raspberrypi /etc/init.d/mysql[20857]: error: 'Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)'

なんでやねん・・・

 

エラーメッセージでググってみると、どうやらsockファイルがないのが原因らしい。

適当にtouchで作ってみるが

raspberrypi /etc/init.d/mysql[20857]: error: 'Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (111)'

あー、直んね。

解決方法

しょうがないので、エラーログを見てみる。

$ sudo cat /var/log/mysql/error.log | grep ERROR

[ERROR] /usr/sbin/mysqld: unknown variable 'default-set-server=utf8mb4'
[ERROR] Aborting

え?この前までこれで動いていたんだが・・・?

unknown variable 'default-set-server=utf8mb4'でググってみると、中国語のサイトにたどり着いた。

どうやら、

default-set-server=utf8mb4

ではなく

character-set-server=utf8mb4

にすればいいようだ。アップデートで変更になってしまったのだろうか。。

これでちゃんと

$ mysql -u root -p

も通るようになった。

まとめ

[client]には
default-character-set=utf8mb4
[mysqld]には
character-set-server=utf8mb4

を設定しましょう。以上。

CodeIgniterのライブラリを使ってレスポンスをスッキリさせる

日本ではあんまり人気がない(らしい)PHPフレームワークのCodeIgniter

軽量だし、MVCモデルがシンプルに使えて拡張できるのはなかなか気に入っているので、

自分で作ったライブラリなんかをメモがてら残しておこうと思います。

 

今回作ったのは

画面表示用のライブラリを作りました。

 

デフォルトのCodeIgniterではviewの呼び出しは

$this->load->view('hoge', $data);

みたいに、コントローラ側でロードしてあげればいいんだが、

例えば

  • ヘッダー
  • コンテンツ
  • フッター

みたいにviewファイルをいくつか呼び出していてコンテンツ部分だけ切り替えたい、みたいな時は、その都度必要なviewを全部呼び出さなきゃいけなくて不便。

せっかくCodeIgniterを使っているんだし、足りないものは自分で作らなきゃね、ってことで

$this->response->html('hoge');

としたら、ヘッダーとかの固定要素は全部読み込んで、共通で渡したい情報もセットできるライブラリを作りました。

 

実装

FWのapplication/librariesの下にResponse.phpを作成します。

htmlメソッドは例えば以下のような感じ。

------------------------------------------------------------

/**
* レスポンスをHTML形式で出力する
*
* @param string $view ビュー名
* @param array $vars 表示データ
* @return void
*/
public function html($view, $vars = array()) {
$vars = array_merge($this->vars, $vars);

$this->ci->load->view('templates/header', $vars);
$this->ci->load->view($view, $vars);
$this->ci->load->view('templates/footer', $vars);
}

------------------------------------------------------------

$varにはレスポンス時に渡すデータ配列が渡せる仕様です。

今回は詳しく書きませんが、別途setメソッドを用意して外部から変更もできるようにしています。

 

このライブラリの中にJSONとかXMLとかあらゆるレスポンス形式のメソッドを作り込んでいけば、どんなレスポンスに対しても

response->***

のような統一感あるレスポンスが書けるはずです。

 

ただ、これを作っただけだとコントローラ側でロードしないと使えないので、

autoloadに書いて、自動で読み込むようにしておきましょう。

CodeIgniterではapplication/configの下にautoload.phpがあるので

$autoload['libraries']の配列内につくったライブラリを追加するとクラスからでも使えるようになります。

 

おわりに

CodeIgniterは拡張性が高いから、欲しいものはどんどん自分で作りましょう!

次回は認証のライブラリでも作ろうかな。