アナログCPU:5108843109

ゲームと音楽とプログラミング(酒と女とロックンロールのノリで)

('ω') < イザユケエンジニャー

CodeIgniter入門 #9:webAPIを作ってみる

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109

今回は、アプリケーションとwebAPIを分離して開発することを想定してサンプルを作ってみます。

今回やること

  • アプリケーションとwebAPIを分離したディレクトリ構造にする
  • webAPIっぽいものを作ってみる
  • アプリケーションのコントローラからwebAPIをコールする
  • アプリケーションの画面フォームからコントローラを呼び出す(間接的にwebAPIをコールする)

ディレクトリ構造

こんな感じにしてみました。
もちろん、/www/index.php と /www/api/v1/index.php の内容も適宜調整します。

  • application
    • web(CodeIgniterのapplication以下そのまま)
    • api
      • v1(CodeIgniterのapplication以下そのまま)
  • system(CodeIgniterのsystem以下そのまま)
  • www

作ってみるAPI

RESTAPIでいってみよう。
参考: REST APIとは? - API設計のポイント

ユーザーテーブル(user)を用意して、それに対して操作するAPIにしようと思います。

  • GET /api/v1/user/ 一覧取得
  • GET /api/v1/user/1/ ID:1を取得
  • POST /api/v1/user/ 登録
  • PUT /api/v1/user/1/ ID:1を変更
  • DELETE /api/v1/user/1/ 削除

userではなくusersにする方が一般的らしいんだけど、今回はこれで。

userテーブルは以下の感じ。(サンプルなのですごい最低限だしパスワードも暗号化しない)

  • id(int / PKでauto_increment)
  • login_id(varchar)
  • password(varchar)

まずはAPIを実装してみる

このURLになるなら、userコントローラを作り、indexの中で処理を分岐させればよいだろうと思う。
ただし、処理の分岐というのは「methodを見分けて適切な関数をコールし分けるだけ」のはずであり、
他のコントローラでも使いまわすはずなので、コアクラスに書くことを考える。

コアクラス

CI_Controllerを継承したMY_Controllerを継承したWebApiControllerを作り、これをコントローラで継承することにする。

<?php
class MY_Controller extends CI_Controller
{
    (省略)
}

class WebApiController extends MY_Controller
{
    public function __construct() { (省略) }

    public function common()
    {
        // methodの判別
        $method = $this->input->method(FALSE);

        // 引数(get,post,streamをマージ)
        $stream = json_decode($this->input->raw_input_stream, true);
        $param_list = array_merge(
            $this->input->get(),
            $this->input->post(),
            !is_array($stream) ? array() : $stream
        );

        // 対応する関数をコール
        if (is_callable(array($this, $method)) && in_array($method, array("get", "post", "put", "delete")))
        {
            echo json_encode($this->$method($param_list));
        }
        else
        {
            echo json_encode(array('msg' => "Undefined API"));
        }
    }
}

特に意味もなく全文貼ったが、このcommonというのは要するに

  • methodを判別して
  • 引数は雑にgetとpostとstreamを全部マージして
  • methodと同名の関数があり、かつそれがget,post,put,deleteのいずれかであればそこに引数を渡す
  • 呼ぶべき関数がなければメッセージを返す

というだけである。

真面目に実装するなら場合によっていろいろ調整が必要かもしれないけど今回はこれで。

ルーティング

config/routes.php

$route['user/(:num)'] = 'user/index/$1';

を追加。
これで、/user/123/ にアクセスすると
userコントローラのindex関数の引数に渡されます。

Userコントローラ
<?php
class User extends WebApiController {

    var $url_param_list = array();

    // コンストラクタ
    public function __construct() { (省略) }

    // API受け口(コアクラスで共通処理を行う)
    public function index($id = null)
    {
        // クエリパラメータ
        $this->url_param_list['id'] = empty($id) ? NULL : $id;

        // 共通処理をコール
        parent::common();
    }

    public function get($param_list)
    {
        $arg_list = array();
        $sql  = "SELECT `id`, `login_id`, `password` ";
        $sql .= "FROM   `user` ";
        if (preg_match("/^[0-9]{1,9}$/", $this->url_param_list['id']))
        {
            $sql .= "WHERE  `id` = ? ";
            $arg_list[] = $this->url_param_list['id'];
        }

        return $this->db->query($sql, $arg_list)->result();
    }

    public function post($param_list)
    {
        $arg_list = array();
        $sql  = "INSERT INTO `user` (`login_id`, `password`) ";
        $sql .= "VALUES (?, ?) ";
        $arg_list[] = $param_list['login_id'];
        $arg_list[] = $param_list['password'];
        $this->db->query($sql, $arg_list);

        return array(
            'id'       => $this->db->insert_id(),
            'login_id' => $param_list['login_id'],
            'password' => $param_list['password'],
        );
    }

    public function put($param_list)
    {
        $arg_list = array();
        $sql  = "UPDATE `user` ";
        $sql .= "SET    `login_id` = ? ";
        $sql .= "      ,`password` = ? ";
        $sql .= "WHERE  `id` = ? ";
        $arg_list[] = $param_list['login_id'];
        $arg_list[] = $param_list['password'];
        $arg_list[] = $this->url_param_list['id'];
        $this->db->query($sql, $arg_list);

        return array(
            'id'       => $this->url_param_list['id'],
            'login_id' => $param_list['login_id'],
            'password' => $param_list['password'],
        );
    }

    public function delete($param_list)
    {
        $arg_list = array();
        $sql  = "DELETE FROM `user` ";
        $sql .= "WHERE       `id` = ? ";
        $arg_list[] = $this->url_param_list['id'];
        $this->db->query($sql, $arg_list);

        return array(
            'id' => $this->url_param_list['id'],
        );
    }
}

また長々と貼っていますが、__construct と index に加え、get, post, put, delete の4つの関数があります。
どこから来てもまずは index で受けることになり、ルーティングで定義されていた引数(ここではID)を確保したのち、あとはさっきのコアクラスのcommonにお任せします。
で、commonからは get, post, put, delete のいずれかにやってきて、必要なクエリを実行したら戻り値を返しておしまい、です。

バリデートなどは無視しているとはいえ、意外にサクッと実装できました。
動作確認は「Advanced REST Client(Chromeアプリ)」などを使えば簡単に行えます。

CodeIgniter入門 #8:データベースの操作<クエリビルダ編・更新系の巻>

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109

前回でだいぶ疲れたのですがせっかくなので更新系もやっておきます。
参照系は基本的に「は?」と思ってたんですが、更新系は「これ上手く使うと効率上がるかも?」と思うところもちらほら。
まあ確かにSQLでもSELECTの方が厄介だよね、普通は…

公式マニュアルはこちら。
クエリビルダクラス — CodeIgniter 3.2.0-dev ドキュメント

今回やること

  • クエリビルダを使ってINSERT・UPDATE・DELETE・TRUNCATEしてみる

INSERT ... VALUES ...(1件のレコード追加)

$data = array(
    'name'     => "ほげほげさん",
    'login_id' => "username",
    'password' => "password"
);
$this->db->insert("user", $data);

おおお…わかりやすい。
これはPHPで書いてると相性良さそう。

オブジェクトも使えるようです。
(公式からの引用)

/*
class Myclass {
        public $title = 'My Title';
        public $content = 'My Content';
        public $date = 'My Date';
}
*/

$object = new Myclass;
$this->db->insert('mytable', $object);

INSERT ... VALUES ... (複数件のレコード追加)

$data = array(
    array(
        'name'     => 'ふがふがさん',
        'login_id' => 'user2',
        'password' => 'pass'
    ),
    array(
        'name'     => 'ぴよぴよさん',
        'login_id' => 'user3',
        'password' => 'pass'
    ),
);
$this->db->insert_batch('user', $data);

おおおお…

UPDATE(1件のレコード更新)

$data = array(
    'name'     => "ほげほげさん_update",
);
$this->db->where("id", 1);
$this->db->update("user", $data);

SELECTと同じく、whereを使って絞り込みができました。

UPDATE(複数件のレコード更新)

$data = array(
    array(
        'id'   => 3,
        'name' => "更新されるふがふがさん",
    ),
    array(
        'id'       => 4,
        'name'     => "更新されるぴよぴよさん",
        'password' => "newpass"
    )
);
$this->db->update_batch("user", $data, "id");

!!!
今回一番の感動。
めんどくさいbulk-updateをいい感じに作ってくれます。
第一引数がテーブル、第二引数がデータなのはこれまで通りで、第三引数に渡したカラム名に対応するデータを使ってくれます。
あっこれならクエリビルダ使ってもいいかも!と思いましたが、しかしbulk-updateやる機会はそんなにないな…。

-- ↑は↓を作ってくれます!!!!!すごい!!!
UPDATE `user` 
SET
  `name` = CASE 
     WHEN `id` = 3 THEN '更新されるふがふがさん'
     WHEN `id` = 4 THEN '更新されるぴよぴよさん'
    ELSE `name`
  END
 ,`password` = CASE
    WHEN `id` = 4 THEN 'newpass'
    ELSE `password`
  END
WHERE `id` IN(3,4)

あればUPDATE、なければINSERT

※タイトル詐欺です。ちゃんと読んでね。

$data = array(
    'id'       => 1, // ここがPK
    'name'     => "ほげほげさん_update_or_insert",
);
$this->db->replace("user", $data);

渡されたデータの中に、primaryかuniqueのカラムがあれば、それを元に「あればUPDATE、なければINSERT」をやってくれます。
やったー超簡単!

……

…………ん? replace?

はい、実際に発行されるクエリは「REPLACE INTO ... 」でした。
これはMySQLにおいて「(あろうがなかろうが)DELETEしてINSERT」する構文です。

例えばレコード作成日時をデフォルトで現在日時にしているとREPLACEした日時にすげ変わりやがりますし、
on updateだって当然反応します。
律儀にアプリ側で日時を渡して記録しているならともかく、ある程度DB任せの設定の場合は危ないのでご注意を。

完全にMySQLの話なので参考URLの紹介にとどめておきますが、
基本的には「INSERT ... ON DUPLICATE KEY UPDATE」構文をおすすめします。
(今回は検証しませんがREPLACEの方が速いという説もありますので、よーーーーく検討して選んでください)

で、ON DUPLICATE... するにはクエリビルダで用意されていないようなので、自分で拡張する必要があるようです。
やっぱりSQLベタ書きでよくない?
【CodeIgniter3】クエリビルダーにINSERT ON DUPLICATE KEY UPDATE構文によるバッチ処理を追加する方法 - あずみ.net

DELETE

さすがにDELETEはシンプルでした。

// DELETE FROM `user` WHERE `id` = 3
$this->db->delete('user', array('id' => 3));

条件の指定方法はWHEREと同じのようで、

$this->db->where("id", 3);
$this->db->delete("user");

でもOKでしたし、なんならFROM句も分離できました。

$this->db->from("user");
$this->db->where("id", 3);
$this->db->delete();

全データ削除

「$this->db->delete("user");」でええんちゃうんかと思ったらダメでした。(うっかり防止?)

ご丁寧に全消し専用メソッドが用意されています。

// DELETE FROM `user`
$this->db->empty_table("user");

TRUNCATEもあります。

// TRUNCATE `user`
$this->db->truncate("user");

DELETEとTRUNCATEの違いはこれまたMySQLの話なので軽くにしておきますが、
DELETEは「レコードを削除する」で
TRUNCATEは「テーブルを作り直す」です。
TRUNCATEの方が速くてキレイになりますが、トランザクションが効きません。
適切な方を選んでください。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.1.33 TRUNCATE TABLE 構文

やっぱりかゆいところに手が届かない…

「INSERT ... SELECT ...」とか「UPDATE ... LEFT JOIN ... SET ...」とかやりたいんですけど一体どうすれば…?
調べてみても有力な情報が見当たらないし、いろいろ試してもみましたが上手くいきませんでした。うーんいまいち。

REST APIの設計むずかしくない?(素人)

最近、SESとしてWeb系案件探すときに「REST APIが作れる人」っていう条件を時々見かける。

気になるので、趣味のサイトをRESTAPIで実装できないかちょっと考えている。
真面目に勉強しろって話なんだけど、とりあえず見よう見まねで。

参考にしているのはこのへん
REST APIとは? - API設計のポイント
RESTのベストプラクティス | POSTD

GET /users/ でユーザー一覧を取得する。わかる。/users/list/とかでもいいっぽいけど。
GET /users/1/ でID1のユーザーを取得する。まあよかろう。
POST /users/ で新規ユーザー登録。ほう。
PUT /users/1/ でID1のユーザー情報の変更。なるほど。
DELETE /users/1/ でID1のユーザーの削除。せやな。

基本はこんな感じっぽい。
なるほどわかりやすいかな、と思ったけど、もっといろいろ考えてるとちょっと詰まった。

たとえばSNSで、
「ログインしていない人が見るID1さんのプロフィール」と
「ログインしている他人から見るID1さんのプロフィール」と
「ID1さんが自分で見るマイページのプロフィール」と
「ID1さんが自分で見る公開用ページのプロフィール」と
SNSの管理者が見るID1さんのプロフィール」って
必要な情報が違うかもしれないよね…。
欲しいデータを絞り込むとかどうとかじゃなくて、権限によって取得自体できないようにしたいよね…。

それどうするんだ?
GET /users/1/ で全部賄うもんなのか?
ログインしている人からは固有のTokenを送ってもらうとすれば確かに判別自体はできるけど…。
まず GETで一覧と詳細の2機能ある時点でそこそこ気持ち悪くない??
そこに権限によっての細かい制御入れるの???
そもそもサイトの仕様をもっとシンプルにしろよって話なの?????教えてエロい人。

CodeIgniter入門 #7:データベースの操作<クエリビルダ編・参照系の巻>

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109


正直PHPよりSQLの方が得意なくらいでクエリビルダは大嫌いなんですが、
仕事で使うことは多い(のにいまいちわかっていない)ので
ひたすら公式マニュアル見て書いて動かしてみました。
クエリビルダクラス — CodeIgniter 3.2.0-dev ドキュメント
いろいろ動かしてみたのを全部書くと上記の公式マニュアルと大体同じになっちゃうので、軽くまとめ直してみました。
それでも長くなったので今回はSELECT編。

今回やること

  • クエリビルダを使ってSELECTしてみる
  • WHERE, LIMIT, ORDER, JOIN, 集計系などある程度カバーする

先に書いておくと

今回と次回でクエリビルダの書き方をいろいろ試していますが、
仕事ではともかく、趣味の開発ではSQL直書きでいくことにします。

上記公式マニュアルには、

クエリを自分で書きたい場合は、データベース設定ファイルでこの クラスを使用できないようにすることもできます。そうすることで、コアの データベースライブラリとアダプタに、少ないリソースを有効活用させることができます。

とありますので、クエリビルダを使わない場合は config/database.php

// クエリビルダを使わないのでFALSEにする
// $query_builder = TRUE;
$query_builder = FALSE;

こうしてやればよさそうです。

とりあえず

まあそういうわけで「SQL直書きで良くねえ?」という感想です。

例えば、「SELECT * FROM `user`」なら

// SQL直書き
$result1 = $this->db->query("SELECT * FROM `user`")->result();
// クエリビルダ
$result2 = $this->db->get('user')->result();

となり、まあ短いしテーブル名だけ書けばいいだけだし…というわけですが…

…いや、実務でそんなクエリほとんど書かないじゃん?

ややこしいクエリになればなるほど、クエリビルダではわけがわからない見た目になる印象でした。
でもやってみる。

現実的(実務で使う程度)なレベルの簡単なSELECT

とりあえずSELECTしてみる

実務で使う程度に簡単なクエリといえばこれくらいですかね。
退会していないユーザーをID降順に50件取得、という感じです。
ページャ想定しての50件として、全部で何件あるかも知れればいいですね。

SELECT `id`, `name`
FROM `user`
WHERE `delete_flag` <> 1
ORDER BY `id` DESC
LIMIT 50, 0

これをクエリビルダで書いてみるとこうなりました。

$this->db->select("id, name");
$this->db->from("user");
$this->db->where("delete_flag <>", 1); // 比較演算子はイコールなら省略できますが、それ以外ならこんな感じになります
$this->db->order_by("id", "DESC"); // ※第二引数は省略するとASCになります
$this->db->limit(50);
$this->db->offset(0);
$result = $this->db->get()->result(); // $result に戻り値が入る

うーん…?
既に微妙な感じが…。
もう少し省略することも可能なのですが(from, limit, offsetは省略してgetの引数に入れることもできる)
省略すればするほど、省略できない書き方になったときに色んな書き方が混在しやすくなります。

全部で何件あるかを調べるには

クエリビルダではできなさそうでした(さっそくつまづく)
やり方あるのかな…ご存じの方は教えてください。
ということでただのMySQLの話になってしまうのですが、以下のように書き換えればOKです。

$this->db->select("SQL_CALC_FOUND_ROWS `id`, `name`", FALSE); // ここを書き換え
$this->db->from("user");
$this->db->where("delete_flag <>", 1);
$this->db->order_by("id", "DESC");
$this->db->limit(50);
$this->db->offset(0);
$result = $this->db->get()->result();
$count_all = $this->db->query('SELECT FOUND_ROWS() AS cnt')->row()->cnt; // $count_all に全件数

selectの第二引数のFALSEは、簡単に言うと第一引数のカラム名をバッククォートでくくるかどうかです。
これを省略(TRUE)していると、内部では「SELECT `SQL_CALC_FOUND_ROWS` `id`, `name` ... 」となってしまいます。
なのでここではFALSEを指定の上、必要なところは自分でバッククォート入れています。

参考: [CodeIgniter] SQL_CALC_FOUND_ROWSを使って一覧と件数を一発で取得する │ M0DE

SELECTで取得してくるカラムやFROMで指定するテーブルに別名を付ける

select内やfrom内にベタ書きでOKでした。

// SELECT `A`.`id` AS `A_id`
// FROM `user` AS `A`
$this->db->select("A.id AS A_id");
$this->db->from("user AS A");
複数指定したいときは…

例えば、WHERE句で複数条件をANDでつないで指定したいとき。
ORDER句やGROUP句の中身を複数並べたいとき。
SELECTの中身が長くなるのでバラして書きたいとき。
こんな感じで並べるだけでOKでした。

// SELECT `id`, `name`
// FROM `user`
// WHERE `delete_flag` <> 1
//   AND `address` = 'tokyo'
// ORDER BY `id` DESC
//         ,`age` ASC
// LIMIT 0, 50
$this->db->select("id");
$this->db->select("name");
$this->db->from("user");
$this->db->where("delete_flag <>", 1);
$this->db->where("address", "tokyo");
$this->db->order_by("id", "DESC");
$this->db->order_by("age", "ASC");
$this->db->limit(50);
$this->db->offset(0);
複数指定したいときは… その2

配列を渡せるケースもあります。

// WHEREだけ抜粋:WHERE `age` < 20 AND `address` = 'tokyo')
$this->db->where(array("age <" => 20, "address" => "tokyo"));
SQL直書きできるケースも多い
$this->db->select("id, name");
$this->db->from("user");
$this->db->where("delete_flag <> 1 AND address = 'tokyo'");
$this->db->order_by("id DESC, age ASC");
$this->db->limit(50);
$this->db->offset(0);

個人的には「は???」って印象でしかないのですが
このような感じで引数にSQL文をそのまま渡すこともできることが多いようです。

WHERE句のバリエーションがすごい

id = 1
$this->db->where("id", 1);
id > 1
$this->db->where("id >", 1);
id > 1 AND age < 20
$this->db->where("id >", 1);
$this->db->where("age <", 20);
id > 1 OR age < 20
$this->db->where("id >", 1);
$this->db->or_where("age <", 20);

えっこれ気持ち悪くない?自分だけ?

id IN (1, 2, 3)
$this->db->where_in("id", array(1, 2, 3));
id IN (1, 2, 3) OR address IN ('tokyo', 'osaka')
$this->db->where_in("id", array(1, 2, 3));
$this->db->or_where_in("id", array("tokyo", "osaka"));
id NOT IN (1, 2, 3) OR address NOT IN ('tokyo', 'osaka')
$this->db->where_not_in("id", array(1, 2, 3));
$this->db->or_where_not_in("id", array("tokyo", "osaka"));

or_where_not_inて。

部分一致検索
// 名前に「ほげ」を含み、住所が「東京」で始まり、メールアドレスが「@gmail.com」で終わる
// (name LIKE '%ほげ%' AND address LIKE '東京%' AND mail LIKE '%@gmail.com')
$this->db->like('name', 'ほげ', 'both');
$this->db->like('address', '東京', 'after');
$this->db->like('mail', '@gmail.com', 'before');

第三引数は、bothの場合は省略できる模様。
もう書かないけど、whereと同じようなノリで or_like, not_like, or_not_like もあります。

集計やグルーピング

GROUP BY
// SELECT group FROM user GROUP BY group
$this->db->select("group")
$this->db->from("user")
$this->db->group_by("group");
DISTINCT
// SELECT DISTINCT group FROM user
$this->db->select("group");
$this->db->from("user");
$this->db->distinct();
集計関数を使う

MIN, MAX, AVG, SUMといった集計関数もちゃんと用意されてました。

// SELECT MAX(age) AS max, MIN(age) AS min, ...
// FROM user
// GROUP BY `group`
$this->db->select("group"); 
$this->db->select_max("age", "max"); // 第二引数は別名(MAX(age) AS max)
$this->db->select_min("age", "min");
$this->db->select_avg("age", "avg");
$this->db->select_sum("age", "sum");
$this->db->from("user");
$this->db->group_by("group");

…COUNT関数は…?(調べてみても見当たらなかった)
仕方ないのでベタ書き戦法です。かゆいところに手が届かないなあ。

$this->db->select("COUNT(`id`) AS `count`", FALSE);
$this->db->from("user");
$this->db->group_by("group");
HAVINGする

HAVINGはサブクエリ使って書くより遅くなりがちなんですけど
HAVING句の使い方と速度検証 - アナログCPU:5108843109
クエリビルダだと複雑なクエリが余計に読みにくくなるし書くのも嫌になってくるので、もうHAVINGでいいかって気になってくる。
いいのかそれで。

// グループごとの合計人数が5人より多いレコードだけSELECT
$this->db->select("COUNT(`id`) AS `count`", FALSE);
$this->db->from("user");
$this->db->group_by("group");
$this->db->having("count >", 5);

WHEREと同じく、比較演算子がイコールなら省略してOKです。

そしてやっぱりor_havingも存在する。

JOINする

// SELECT user.id AS user_id, log.id AS log_id
// FROM user
// LEFT JOIN log ON user.id = log.user_id
$this->db->select("user.id AS user_id, log.id AS log_id");
$this->db->from("user");
$this->db->join("log", "user._id = log.user_id", "left");

join句の第三引数で「left」を指定していますが、もちろん必要に応じてinnerも指定できます。
rightとかもあるけど使わんかろ。

複数JOINする場合は例によって並べるだけでした。

// SELECT user.id AS user_id, log.id AS log_id, log_category.id AS log_category_id
// FROM user
// LEFT JOIN log ON user.id = log.user_id
// LEFT JOIN log_category ON log.category = log_category.id
$this->db->select("user.id AS user_id, log.id AS log_id");
$this->db->from("user");
$this->db->join("log", "user._id = log.user_id", "left");
$this->db->join("log_category", "log.category = log_category.id", "inner");

また、ON句の条件を複数にしたい場合は…クエリビルダっぽい書き方は見つけられませんでした。
ベタ書きで動きます…。

$this->db->join("log", "user._id = log.user_id AND delete_flag <> 1", "inner");

とても複雑なクエリを書く

めんどくさいので公式マニュアルからの引用です。

クエリのグルーピングでは、 WHERE 句を括弧で囲むことでグループを作ることができます。 これにより複雑な WHERE 句のクエリを作ることが可能です。
例:

$this->db->select('*')->from('my_table')
        ->group_start()
                ->where('a', 'a')
                ->or_group_start()
                        ->where('b', 'b')
                        ->where('c', 'c')
                ->group_end()
        ->group_end()
        ->where('d', 'd')
->get();

// 次のようになります:
// SELECT * FROM (`my_table`) WHERE ( `a` = 'a' OR ( `b` = 'b' AND `c` = 'c' ) ) AND `d` = 'd'

お、おう…。
引数にクエリをベタ書きするのを避けるならこうなるんですかね。
これくらいのネストができるクエリって結構あると思うんですけど、慣れてないと読みにくいっすね…。

クエリビルダが嫌いな理由

突然の愚痴

データベースのエラーとかスローログとかを元に調査するとき、検索性が悪くて困る…。
自分が書いたコードなら大体の場所は見当つくかもしれないけど、そうでないと結構むずい。
これがSQL直接書いてるだけなら特徴的な部分を抜き出して一発grepかけるだけなんだけど。
何かいい方法あるのかなこれ。

あと、クエリビルダ自体が悪いわけじゃないんだけど、
こっちのほうが書きやすい・見やすいって言う人は「そもそもSQLやデータベースそのものがよく分かっていない」っていうケースが多い。
SQLを理解せずに見よう見まねのクエリビルダで書かれるとヤバいコードが生産されがちで困る。
(ありがちなのが、JOINの概念が分からなかったのか適当にSELECTしてきたデータをアプリ側でつなぎ合わせてるとか、集計関数を知らないのか同じくアプリ側で集計しなおしてるとか)
データベースの知見がある上でクエリビルダを使いこなしている方も当然いるんだろうけど、いくつかの現場を見た限り出会うことはなかったのでたぶん少数派。


うーん、使うメリットがわからん…。今は嫌いだから使いたくないけどメリットがあるのならもっと考えたいから誰か教えてくれ。
データベースの種類ごとまるっと乗り換える(MySQLからPostgreSQLにするとか)場合は修正箇所が少なくて済む、とかもあるんだろうけど、日頃の使い勝手悪い方が困るわ。そんな機会そうそうないでしょ。
(と言いつつ今やってる案件ではあるんですが(乗り換えではないけど、ほとんど同じシステムのDB違い版がある)結局レアケースだろうし…)
MySQLだろうがOracleだろうが書き方が変わらないので覚えることが少なくて済む…かもしれないけど、クエリレベルの話なんだからせいぜい方言差だし、どっちかというとフレームワークごとのクエリビルダの違いの方が大きいんじゃないのか??
SQL知らなくても使えるっていうのは先述のとおりメリットよりデメリットの方が大きそうだし。

複数のファイルをフォルダごとコピーするスクリプト

今の現場、開発サーバーにソースコード上げる作業がかなりめんどくさいので、ちょっとだけスクリプト化しました。

gitにコミットしても自動で反映されるわけじゃない。
しかもFTPとかじゃなくてリモート接続。

なので、

  • サーバー側の変更対象ファイルをバックアップしておく
  • バックアップしたファイルとローカル側のファイルを差分チェックする
  • ローカル側のファイルをサーバーに設置する
  • 動作確認とかする
  • 場合によっては元に戻したりする

とかいう手順を踏むわけです。

このうちサーバー側の必要ファイルバックアップと、ローカルからサーバーに持っていくファイルを抽出する作業が地味にめんどくさいのですが
この二つの作業が大体同じなので、その部分だけスクリプト化してみました。
ローカルもサーバーもWindowsなのでまあVBSで。

やっていることは、「テキストファイルに一覧化したファイルを全部コピーして、フォルダ構造も再現しつつ指定したフォルダにペースト」です。
例えば、

C:\hoge\fuga\a.txt
C:\hoge\piyo\b.txt

というリストを作り、「C:\backup\」にコピーするようにしてスクリプトを実行すると、

C:\backup\hoge\fuga\a.txt
C:\backup\hoge\piyo\b.txt

にコピーされてくる、という感じです。
フォルダ階層が深いときはコピー先も無駄に深くなるけど…。

これさえあれば、このスクリプトでバックアップ対象と更新対象を抽出して、そのフォルダごとWinmergeで確認して、更新対象フォルダをサーバー側に放り込むだけで済みます。
もちろん元に戻すときはバックアップしておいたフォルダを放り込むだけ。
(とはいえフォルダを放り込むだけなので、更新にしろ元に戻すときにしろ、削除が発生する部分は手作業ですが…)

使い方

  • テキストファイルに、コピーしたいファイルをフルパスの改行区切りで並べておく
  • そのテキストファイルのパスと、コピー先のディレクトリパスを下記スクリプト内で設定する
  • スクリプト実行

プログラム

続きを読む

CodeIgniter入門 #6:データベースの操作

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109


まあ公式マニュアルにまとまってるんですけどね。
データベースへの接続 — CodeIgniter 3.2.0-dev ドキュメント
クエリ — CodeIgniter 3.2.0-dev ドキュメント

今回やること

  • コントローラからデータベースの操作(SELECT、INSERT、UPDATE、DELETE)を行う
  • プレースホルダを使う
  • エスケープする

データベース

データベースはこんな感じ。
ユーザーマスタ的なものを想定。
とはいえお試しなので超シンプルに、users テーブルに、連番の id と文字列を入れる name で。

CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(16) NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB;

データベースへの接続

以前書いたことと少しかぶるので簡単に。
まず、設定値は config の database.php へ。
そこだけ設定しておけば、コントローラ内に

$this->load->database('default');

と書くだけで接続できます。

ほとんどのコントローラで接続を行う場合など、これをいちいち書きたくない場合は config の autoload.php のライブラリに「database」を追加しておけばOKです。

$autoload['libraries'] = array(
	'database',
	// 他にもライブラリ系を毎回ロードしておきたい場合はここに追加するものと思われる
);

INSERTする

テストデータを手動で入れるのすら面倒なので、INSERTから作ります。

今回はUserコントローラを用意し、その中に insert 関数を設置しました。
/user/insert/ でアクセスできます。

一番シンプルなのはこれだけ。

public function insert()
{
    $sql  = "INSERT INTO `user` (`name`) ";
    $sql .= "VALUES('なまえ') ";
    $this->db->query($sql);
}

SELECT

次はINSERTされているかどうか確認するためにもSELECT作ります。

とりあえず全行SELECTしてくるやつ。

public function select()
{
    $sql  = "SELECT `id`, `name` ";
    $sql .= "FROM `user` ";
    var_dump($this->db->query($sql)->result());
}

UPDATE

無事にINSERTしたやつがSELECTできることを確認できたので、次はUPDATEしてみます。
変わり映えしないですが…。

public function update()
{
    $sql  = "UPDATE `user` ";
    $sql .= "SET `name` = '変更後だよ!!!!!' ";
    $sql .= "WHERE `id` = 3 ";
    $this->db->query($sql);
}

SELECT動かして変更されることを確認。

DELETE

追加・変更・取得ときたので最後は削除。

public function delete()
{
    $sql  = "DELETE FROM `user` ";
    $sql .= "WHERE `id` = 3 ";
    $this->db->query($sql);
}

これで一通り動かしてみることができました。

プレースホルダを使う

値を動的に組み込みたい場合はプレースホルダを使います。
もちろんエスケープ等はCodeIgniterさんがやってくれるのでこれだけで安全になります。
INSERTのコードの、VALUES句の中身を動的にしてみます。

public function insert()
{
    $sql  = "INSERT INTO `user` (`name`) ";
    $sql .= "VALUES(?) ";
    $this->db->query($sql, array("ほげ"));
}

query の第二引数に渡した値が「?」に入ってくれます。

複数にも当然対応しています。
例えばSELECTで検索条件やページャ用の取得件数制御を入れてみます。

public function select()
{
    $sql  = "SELECT `id`, `name` ";
    $sql .= "FROM `users` ";
    $sql .= "WHERE `id` > ? ";
    $sql .= "LIMIT ?, ? ";
    $arg_list = array(
        1,
        0,
        5,
    );
    var_dump($this->db->query($sql, $arg_list)->result());
}

この場合はクエスチョンの順番通りに当てはめられていきますので、「WHERE `id` > 1 LIMIT 0, 5」となります。

明示的にエスケープする

escape メソッドを使えばクエリの文字列にエスケープした文字列を結合することもできます。
動的な値を入れるときはプレースホルダが使えればOKなんですけど、
テーブル名やカラム名を動的にするときなど場合によっては必要になるかも…?

public function insert()
{
    $sql  = "INSERT INTO `user` (`name`) ";
    $sql .= "VALUES(" . $this->db->escape("'',''ふが~~") . ") "; // なんか危なそうな文字列をエスケープして結合
    $this->db->query($sql);
}

CodeIgniter入門 #5:ログが記録されるようにする

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109

今回やること

  • エラーページを設置する
  • エラーログが記録されるようにする

謎エラーが出た

なんかね、ちょっとコントローラがつがつ書いてたら、なんかミスってたらしくエラーが出たんですよ。
エラー内容読む前に「あそこ間違えてるわ」と気付いて直してみたものの、エラーが消えない。

Warning: include( ... \application\errors\html\error_php.php): failed to open stream: No such file or directory in  ... \system\core\Exceptions.php on line 268

… application/errors/html/error_php.php が存在しない?
ええ、ないですけど。
最初からそんなのないんですけど。

自前でとりあえず適当に設置してみたらそれが呼び出されました。
どういうこと???勝手に呼び出しておいて自分で設置する前提???
エラー処理 — CodeIgniter 3.2.0-dev ドキュメント

コードを漁ってみると、error_phpの他に、error_general, error_db, error_404, error_exception が呼び出される可能性があるっぽい?
あと、htmlディレクトリの他にcliディレクトリも。
うーん。また必要になったら考えることにする。同じように設置すればいいはず。

エラーログが更新されてなかった

で、自分で設置したファイルはほぼ空なので、結局本来のエラー内容が不明。
logsディレクトリを見てみても特に何も入っていない。

これは config/config.php 内に設定があったので以下のように調整。

$config['log_threshold'] = 1;

これでlogsディレクトリ以下にエラーログが記録されるようになりました。

CodeIgniter入門 #4:自作コアクラスを挟んでみる

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109


CodeIgniterにデフォルトで入っているWelcomeコントローラは以下のように始まっています。

class Welcome extends CI_Controller { ...

これはsystem(コアシステム)内のクラスを継承しているのですが、
いろんなところで共通の処理を独自に入れたい、というときにsystem内をいじるのはあまりよろしくありません。
CI_Controllerを継承した独自のクラスをさらに継承して使いたいと思います。

今回やること

  • コアクラスとコントローラの間に自作のクラスを挟む

まずはドキュメントを見る

コアシステムクラスの作成 — CodeIgniter 3.2.0-dev ドキュメント

要約すると…

  • application/core の下に自作クラスを設置する
  • クラス名は「MY_」で始めること
    • application/config/config.php 内で変更は可能、ただし「CI_」は不可
  • コントローラの継承元を自作クラスにする

…という感じですね。

やってみる

application/core/TestClass.php

<?php
class MY_Controller extends CI_Controller {
}

application/controller/Welcome.php

<?php
class Welcome extends MY_Controller {
	public function index()
	{
		echo "test";
	}
}

まずはシンプルに用意してみたのですが…

…動かない…。

Fatal error: Class 'MY_Controller' not found in ... Welcome.php on line 3

と言われてしまいます。

作ったクラスが読み込まれていないっぽい?

エラーメッセージでググってみる。

CodeIgniter Coreファイルが見つからない | ZEKIOM.NET
php - codeigniter MY_Controller not found - Stack Overflow
php - CodeIgniter 3.1.7 - Class 'MY_Controller' not found error occurred only on my server - Stack Overflow

解決していたりしていなかったりですが、このへんを参考にしつつ、
試しにWelcome.phpの先頭に

include_once(APPPATH.'core/TestClass.php');

を入れてみたら動きました。
どうやら読み込まれていなかった様子。デフォルトでは読まれないのか?
じゃあどこで読み込むべきなんだろうかとソースコードgrepしていると、
system/core/CodeIgniter.php 内にこんなものを見つけました。

if (file_exists(APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php'))
{
	require_once APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php';
}

これはまさかとクラスのファイル名をMY_Controller.phpにしてみたら、動いた。

えええ…。
でも公式ドキュメントの例だと some_class.php とかいう適当な名前なんですけど…。
coreディレクトリ以下をまるっと読んだりしてくれないの?

複数ファイルを使い分けたりできないのかと考えるとしっくりこないのでもう少し探してみたのですが、
system/core/Common.php 内の load_class とかいう関数が怪しいかなーと思いつつ、これもまたしっくりこないので諦めた。
とりあえずMY_Controller.php で乗り切ることにします。

MY_Controller.php をなんとか使いまわす方法

この方法が正しいのかどうかはわかりませんが…
Step 9 - No more MY_Controller monopoly. How you can create more than one base controller - Avenir
を参考に。

複数のクラスを使いたくなったら、こんな感じで乗り切るのがお手軽かも。
ロードする順番に気を使わなくて済むし。

<?php
class MY_Controller extends CI_Controller
{
    // ...
}
 
class A_Controller extends MY_Controller
{
    // ...
}
 
class B_Controller extends MY_Controller
{
    // ...
}

Mercuryを使ってみる

XAMPPにくっついてくるMercury、今まで使ったことがなかったのですがついに仕事で使う機会があったのでメモ。

ローカル環境でメール送受信ができます。

Mercuryがローカルでメールサーバーとして動く状態にする

  • XAMPP内のmercury.exeを管理者権限で起動
    • もしくは管理者権限で起動したXAMPPのMercuryのAdminをクリック
  • Contigration > Protocol modules にて以下にチェックを入れる
    • MercuryS SMTP server
    • MercuryP POP3 server
    • MercuryE SMTP end-to-end delivery server
  • Contigration > MercuryS SMTP Server にてIPアドレスの設定を行う
    • Connection control タブで「Add restriction」をクリック
    • IP Address range のfromとtoを両方「127.0.0.1」に設定する
  • Contigration > MercuryP POP Server にてIPアドレスの設定を行う
    • Connection control タブで「Add restriction」をクリック
    • IP Address range のfromとtoを両方「127.0.0.1」に設定する

メールアドレスを追加する

  • XAMPP内のmercury.exeを管理者権限で起動
    • もしくは管理者権限で起動したXAMPPのMercuryのAdminをクリック
  • Contigration > Manage local users... をクリック
    • 「Add」をクリックして出てきたウインドウで以下を設定
      • Username:メールアドレスの@以前の部分
      • Personal name:Usernameと同じでよい
      • Mail password:パスワード
      • APOP secret:Mail passwordと同じでよい

ドメインを設定する

  • XAMPPのsendmail/sendmail.ini にて以下のように書き換える
    • smtp_server=hogehoge.foo
      • hogehoge.fooの部分は別のものでもよいが、このように存在しないドメインを使うなどしておくと安全
  • XAMPPのMercuryMail/MERCURY.INI にて以下のように書き換える
[Domains]
localhost: localhost
localhost: localhost.net
localhost: localhost.org
localhost: localhost.com
localhost: hogehoge.foo ; # この行を追加

動作確認

メールソフト等で送受信ができるかどうか確認して終了

CodeIgniter入門 #3:複数サイトの設置に対応してみる

CodeIgniter入門シリーズ カテゴリーの記事一覧 - アナログCPU:5108843109

入門どころかいろいろすっ飛ばして応用してる感がありますが

なんかうまいこと複数サイトを運用する方法あるかな、と思って考えてみました。
レンタルサーバーなどドキュメントルートが一つなので共有するしかない、みたいな前提)

といってもかなり直球な方法なので大仰に書くほどでもないのですが…

今回やること

  • 複数サイトを想定してディレクトリ構造を変更し、動かしてみる

ファイル設置してみる

こんな感じで。

  • codeigniter
    • application
    • application_a
    • system
  • www(←ドキュメントルート)

前回の構造は基本的にそのままで、「applicationディレクトリを複製」「index.php.htaccessのセットを複製したやつを別ディレクトリ切って設置」しています。
systemは共有して良さそうに見えるのでそのままにしていますが、問題があればapplicationと同じ対応でよいはずです。
逆にapplicationも共有する場合はそもそも複製しなくてOK…なのでしょうけど、そんな運用あるかな…

ドキュメントルート下にcssやjsなどのディレクトリを切る場合もまあ好きなところでよいでしょう。

っていうかcodeigniterディレクトリっていうのがだいぶダサくなってきたな…。

アクセスできるようにしてみる

複製しただけなので、まずwww直下のindex.phpは何も変わりません。「/」でアクセスできます。
で、複製したところは「/a/」でアクセスできるようにしたいと思います。

といっても、www/a/index.php を実際のパスに合わせて以下のように調整するだけです。

$system_path = '../../codeigniter/system';
$application_folder = '../../codeigniter/application_a';

/a/ に無事アクセスできればOK。あとはそれぞれのapplicationディレクトリ以下で開発するだけ。

URLがかっこ悪いっていうのは…独自ドメインとかあればサブドメインに置き換えるとか上手いことできるもんなのかな?(よく知らない)

想定する用途

単に丸々別物のサイトの運用もできるでしょうけども、例えば
「/」は一般公開用ページとして、
「/user/」はログイン中専用ページ、「/admin/」は管理ページ、「/api/」はwebAPI用…みたいな感じでもよさそう。
adminとかapiとかってコントローラ作るのも何だし、だからって.htaccessなどでいじり回すのもアレだし。
そういう分け方しておくと、いざ複数サーバーに分散設置したいってときもちょっと楽そうだし。