アナログCPU:5108843109

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

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

【更新中】ななどら外伝2人旅シリーズ #9:トリックスター×サイキック(2021)

セブンスドラゴン2021でトリ×サイ2人旅。

honey8823.hateblo.jp

キャラメイク

前回↓から続投。シズクちゃんは銃縛り。
ななどら外伝2人旅シリーズ #7:トリックスター×サイキック(2020) - アナログCPU:5108843109

プロローグ(Lv1~)

  • 最初のマモノ討伐はLv4で完了
  • D201:Lv7 / 11ターン
  • D202:Lv8 / 12ターン
  • プレイ時間:1時間57分

二人揃って燃費の悪さがつらい。

1章(Lv8~)

  • ツインホーンドラグ:Lv8→9 / 11ターン / TAKE2
  • エンシェンタス:Lv11 / 21ターン / TAKE3
  • ティアマット:Lv14→19 / 36ターン / TAKE6
  • プレイ時間:5時間11分

ツインホーンドラグの攻撃が痛いのでさっさとLIFEボーナスを1ずつ取得。

ティアマットはLv14では全く太刀打ちできそうになかったのでLv18までレベリング、
それでもキツかったので(回復アイテムが足りなくなる)Lv19にして、
さらにデコイミラーをLv2にすればかなり安定。
(Lv2にすれば一発は耐えられるようになる)

この時点のスキル
 レイ:LF1、フレイム1、エナジーピラー2、デコイミラー2
 シズク:LF1、エイミングショット4

幕間1(Lv19~)

  • SECT11:Lv21&22 / 9ターン
  • プレイ時間:6時間25分

ティアマット戦のためにLv上げすぎたのか、SECT11はあっさり撃破。
スキルは特に何も上げず。

2章(Lv22~)

  • ドラグメガマウス:Lv25 / 5ターン / TAKE2
  • オケアヌス・テイル:Lv26 / 4ターン
  • オケアヌス:Lv29→30 / 10ターン / TAKE4
  • プレイ時間:9時間26分

苦戦しがちなドラグメガマウス、今回は意外とあっさり突破。

オケアヌスはLv29でたどり着いたもののあまりにも攻撃が痛く、Lv30に上げつつデコイミラーをLv3にしてクリア。

この時点のスキル
 レイ:LF1、フレイム1、ヒートボディ3、エナジーピラー2、デコイミラー3
 シズク:LF1、エイミングショット6

3章(Lv30~)

【更新中】ななどら外伝2人旅シリーズ #8:トリックスター×トリックスター(2020)

セブンスドラゴン2020トリックスター2人旅。


キャラメイク

武器は縛りなし。なんでもアリの代わりにLv55程度でのクリアを目指します。

0章(Lv1~)

  • D1:Lv2 / 5ターン
  • D2:Lv3 / 6ターン
  • プレイ時間 0時間43分(※キャラメイクでのんびりしていた)

ここはさらっと一発クリア。

1章(Lv3~)

  • ドラゴハンマード:Lv4→6 / TAKE3 / 8ターン

ななどら外伝2人旅シリーズ #7:トリックスター×サイキック(2020)

セブンスドラゴン2020トリックスターとサイキックの2人旅。


キャラメイク

短剣トリスタは強すぎるので銃縛り。

0章(Lv1~)

D1・D2 全然メモってなかったけどさらっと一発クリア

1章(Lv4~)

ドラゴハンマード、メモってなかったけど一発クリア

  • ウォークライ:Lv12、TAKE5で撃破(10ターン)

ウォークライはLv9で到達したけどさすがにキツかったのでレベリングしつつ挑戦。
もはや恒例、二人ともLFボーナス2を取れたら突破。

1.5章(Lv12~)

全然メモってねえ。特に苦戦するポイントなし
基本的にレイくんはデコイを切らさないようにして手が空いたら攻撃、シズクちゃんは二人ともHPに余裕があるときだけ攻撃、というとにかく耐えるスタイルなのでちょっと時間はかかる
シズクちゃんにエスケイプスタンスを搭載

2章(Lv16~)

  • ジゴワット:Lv21で撃破(25ターン)
  • SKY:Lv21で撃破(11ターン)

ジゴワットはややターン数がかかったものの一発クリア。
二人ともLFボーナス3を取って、残ったSPは、レイくん→フリーズ全振り・シズクちゃん→エイミングショット全振り。
SKYは今回もネコから狙う。

3章(Lv21~)

  • デストロイドラグ:Lv25、TAKE9で撃破(7ターン)
  • ロア・ア・ルア:Lv28、TAKE3で撃破(13ターン)

デストロイドラグで詰むかと思った。ギリギリセーフ。
Lv22で到達してレベリングしつつ挑戦しました。
レベルというよりはスキル不足で、ヒートボディ習得&エイミングショットに盲目が乗るようになったところで勝利。
ロア・ア・ルアはレベリングはせず、「勝てそうで勝てない」からの2回リトライでクリア。

4章(Lv28~)

  • SKY:Lv32、TAKE4で撃破(15ターンくらい:メモ忘れ)
  • トリニトロ:Lv35、TAKE4で撃破(34ターン)

SKYは珍しくタケハヤ→ダイゴ→ネコの順。ヒートボディのおかげでタケハヤが勝手に倒れていく。
トリニトロはLv34→35にレベリングしてクリア。

5章(Lv35~)

  • スリーピーホロウ:Lv38で撃破(11ターン)

一発クリア。対策さえしてれば特に怖くない。

6章(Lv39~)

  • ザ・スカヴァー(尾):Lv42で撃破(7ターン)
  • ザ・スカヴァー(胴):Lv42、TAKE2で撃破(12ターン)
  • ザ・スカヴァー(頭):Lv43、TAKE4で撃破(12ターン)

胴と頭の間で思い出したように奥義を取りに行く。ワジさんごめん。
この時点で、
レイくんはデコイ・ヒートボディ・ゼロ℃ボディ・フリーズ・プラズマジェイル+奥義
シズクちゃんはエイミングショット+奥義(&エスケイプスタンス)
という感じ。スキルが少なすぎる。
「根本的に無理ってレベルで苦戦したらスキル増やそう」という方針で進めてみたらこんなことに…。
サイキックが回復する暇がなくて未だに回復スキルが一切ない。

7章(Lv43~)

  • ゼロ・ブルー:Lv49、TAKE3で撃破(18ターン)

レベリングはせずになんとか突破。
1回目はボコボコにされたけど、デコイをMAXにして再挑戦してみると1発は耐える場面が増えて余裕が出た感じ。
クリティカルリアクト&ラッシュショットとかも取得はしてみたけど使う暇はなかった。

X章(Lv49~)

  • ミヅチ:Lv55で撃破(33ターン)
  • すべてのドラゴンを狩りつくした!
  • ニアラ:Lv64、TAKE7で撃破(21+4ターン)

ちょっと様子見で突撃~とやってしまったミヅチ、今回も一発クリア。なんも怖くねえな…
スキルは以下
レイ:LF4、ヒートMAX、フリーズ4、ゼロ℃2、プラズマMAX、デコイMAX、オートリカヴァ、奥義
シズク:LF4、ラッシュ3、エイミングMAX、リアクト、エスケイプ、奥義

ニアラはLv59で3回、Lv62で2回、Lv64で1回hage。
スキルは以下(使ったやつだけ抜粋)
レイ:LFMAX、ヒートMAX、ゼロ℃MAX、デコイMAX、マイクロバースト3、コンセントレートMAX、オートリカヴァ、奥義
シズク:LFMAX、MANAMAX、ラッシュMAX、エイミングMAX、リアクト、奥義

Wordで同人小説原稿を作る

一般的な小説っぽい形式の原稿を作るまで。

なにはともあれWordを開く

でっかいな。
f:id:honey8823:20200318200118p:plain:w400

紙のサイズを設定する

レイアウト>サイズ から設定します。欲しいサイズがなかったら「その他の用紙サイズ」から手入力。
f:id:honey8823:20200318200223p:plain


f:id:honey8823:20200318200336p:plain
うん…。

余白を設定する

レイアウト>余白 から設定します。今回はこんな感じ。
f:id:honey8823:20200318200721p:plain

f:id:honey8823:20200318200831p:plain
お、それっぽいサイズ感。

縦書き・行数・フォント・文字サイズを設定する

文書の上で右クリック>縦書きと横書き から縦書きに設定します。
f:id:honey8823:20200318201014p:plain
f:id:honey8823:20200318201058p:plain

行数は、Altを押しながら「P」>「S」>「P」と押せば開く「ページ設定」にて設定します。

f:id:honey8823:20200318203435p:plain

また、このダイアログ右下の「フォントの設定」からデフォルトのフォントと文字サイズを設定できます。

f:id:honey8823:20200318202449p:plain

もし、行数設定がうまく反映されない(行間が空いて少ない行数しか入らない)場合は、
Altを押しながら「O」>「P」で開く「段落」ダイアログにて、
「1ページの行数を指定時に文字を行グリッド線に合わせる」のチェックを外します。

f:id:honey8823:20200318203703p:plain

f:id:honey8823:20200318204040p:plain

ノンブル(ページ番号)をつける

今回はヘッダーに付けていきます。
白紙の上の方をダブルクリックすればヘッダー編集のメニューが開くので、そこでページ番号指定や位置調整を行います。
f:id:honey8823:20200318204258p:plain

右ページは右端、左ページは左端…というように左右で変えたい場合は、「奇数/偶数ページ別指定」にチェックを入れると
左右が連動しなくなるので、それぞれで編集します。

また、開始ページ数は、ページ番号>ページ番号の書式設定 から設定できます。

f:id:honey8823:20200318204950p:plain

ひとまず完成です!

おまけ:ページ番号横に章タイトルを並べる

普通にヘッダをいじってしまうと、前ページに反映されてしまいます。
章ごとにタイトルを付けたい場合は以下のようにします。

まずタイトルをつけてみる

ヘッダーをダブルクリックで編集できます。
f:id:honey8823:20200318205729p:plain

章ごとに「セクション」で区切る

章の最後では、レイアウト>区切り>次のページから開始 でセクション区切りを挿入します。
f:id:honey8823:20200318205255p:plain

区切った先のヘッダーを編集する

まず、セクション区切り前後でページ番号がリセットされている場合、改めてページ番号設定を行います。
本来のページ数を設定するか、もしくは「前のセクションから継続」でOKです。

章タイトルは前のセクションのものが入っていますが、これをそのまま変更すると、前のセクションにまで影響してしまいます。
ここで、ヘッダーを変更する前に「前と同じヘッダー/フッター」をオフにします。
f:id:honey8823:20200318205918p:plain
オフにした後で編集すれば、セクション区切りの前と後で異なるタイトルにすることができます。

f:id:honey8823:20200318210345p:plain

おまけ2:PDFに出力する

印刷からPDF出力すると何故か紙のサイズがA4か何かに固定されることがある?ようなので、

ファイル>エクスポート からPDF出力するとよいようです。ファイル名を付けるタイミングで「オプション」からページ範囲の指定なんかも可能。

2度目の転職から1年8か月経ちました

前回のあらすじ(?)

SESなWebエンジニアとして3つ目の現場で働いているところですが、
早くも3度目の転職を考えることになりました。
といっても今の会社にはさほど不満はなく、今の現場で引き抜きのお誘いを受けたのです。

給与は今より上がることが前提ですよとは伝えたし、勤務時間は変わらないし、家から近いし、実力主義裁量労働感あって価値観近そうだし、雰囲気も悪くないし、有休とかも普通に取りやすそうだし。今までの勤務先・出向先全部合わせても自分の中ではかなり上位。
技術的にも悪くない。色々試せる風土はありそうだし、DBもSQLServerPostgreSQL使っててまだまだ勉強になりそう。
デメリットはSESでなくなること(いろんな現場を渡り歩いて武者修行できなくなること)かなあ。あと、すごい雑な言い方すると、SESだと人間関係割り切って開発業務だけしてればいいから好きではあるし。今の会社来るまでSES敬遠してたけど意外と楽しかった。電話対応とか雑務とかしたくねえ~って感じ。
あと自社は自社でいいとこなので申し訳なさもある…。いい人ばっかりなんだよなあ…

つーか引き抜きってマジであるんだな…仲介業者との関係とかの大人の都合は大丈夫なのかな…と思ってたら、どうもその業者経由で来てた人を既に何人か引き抜いてるらしい。いいんだ…。それで普通に関係継続してるんだ…。
(自分みたいに、その業者の直接の所属じゃない人ばかりだったのかもしれませんが)

年収次第なところはあるのでまだ保留ですけどね。
フリーランスになって直接契約」を提案するのが一番効率良さそうな気がしたけどフリーランスこわい。

<2020年6月追記>

コロナで若干うやむやになっていましたがちょっと落ち着いてきて話が進み、
「転職は個人の自由とは言えど、こっそり引き抜くのは不誠実なので所属会社に相談してもらえます?」と言われ
「引き抜きの打診を受けたんですけど~」と素直に営業さんに相談。
自分で言うのもアレですけど自社ではかなり高く評価いただいているので引き留められるかなーと思いきや、
「そちらに転職してもらえれば直接営業かけられるパイプができるのでそれはそれで!」とある意味で営業さんらしい回答。まあ今は間に2社挟んでますからね。うまくやれればむしろプラスになるかもですね。
現場の社員さんからしても、わたしを介して指示しやすいSESが確保できれば嬉しいとのことで、まさかのwin-win(?)。
営業さんからは「あっでも年収提示されたら教えてくださいね! そりゃ引き留められるに越したことはないので社長に交渉します!」とのこと。
…うーん…自社、給与テーブルががっちり決まってる会社なので交渉できるかどうかは知らんけど…っていうかそれで自社に残ることになってもなんか気まずいんですけど…。完全に金で動く奴みたいじゃん。動くけど。この労働環境で年収50万くらい上げてくれたらホイホイついてくけど。

引き抜きとはいえ人事面接とかはやるので、そこで落ちなきゃいいな。

CodeIgniterでSPA実装してみる

SPA is 何

参考:SPA(Single Page Application)の学習、そこに高度なスキルなどいらない - Qiita

Single Page Applicationの略。
一枚のHTMLの中でJS使ってがちゃがちゃ書き換えようぜっていう思想です。

FacebookとかTwitterみたいなやつね。

CodeIgniterで実装してみた

参考:ui-routerとcodeigniterを組み合わせてSPAをつくるときのメモ - よっこらせとプログラム

JSのプラグインとかもそのへんに落ちてるみたいですが、今回はCodeIgniter+jQuery環境で自分で作ってみました。
(別にjQueryである必要はないです)

とりあえずサンプル完成品を貼っていく。
適当に抜粋してるので適当に解釈してください。

仕様

「/sample/test_a/」「/sample/test_b/」の二つのページを行き来するだけのもの。
それぞれに互いへのリンクが張ってあります。
で、もちろんSPAなので中身のコンテンツ部分だけを書き換える感じ。
もちろん、各URLに直接アクセスしたときもそれぞれのページが表示されるようにするし、ブラウザのURLや履歴もちゃんと更新する。

作ったのは

  • core/MY_Controller.php
  • controllers/Sample.php
  • views/top.html
  • views/test_a.html
  • views/test_b.html

core/MY_Controller.php

やってること

  • viewに渡すパラメータ変数はここで定義している
  • __construct():spaっていうパラメータに「on」が渡されたらspaフラグを立てる
  • view():spaフラグが立ってたらコンテンツ部分だけを返し、立ってなければ普通にview()する
  • spa_view():viewをちょっと拡張して、必ず結果文字列を返すようにしたやつ

controllerをシンプルにしたいあまりここがごちゃごちゃに。
実際に使うとなったらもうちょっと切り分けなきゃいけないケースが出てくるかもしれない。

<?php 
class MY_Controller extends CI_Controller {

	var $spa = FALSE;

	var $data_list = array('content' => NULL);

	public function __construct()
	{
		parent::__construct();
		if ($this->input->get("spa") == "on")
		{
			$this->spa = TRUE;
		}
	}

	public function view($template_name = "top")
	{
		if ($this->spa == TRUE)
		{
			echo $this->data_list['content'];
			return;
		}
		$this->load->view($template_name . ".html", $this->data_list);
	}

	public function spa_view($template_name)
	{
		return $this->load->view($template_name . ".html", $this->data_list, TRUE);
	}
}

controllers/Sample.php

やってること

  • test_a():test_aというviewの中身を取ってview()するだけのやつ
  • test_b():test_aというviewの中身を取ってview()するだけのやつ
<?php
class Sample extends MY_Controller {

	public function test_a()
	{
		$this->data_list['content'] = $this->spa_view("test_a");
		$this->view();
	}

	public function test_b()
	{
		$this->data_list['content'] = $this->spa_view("test_b");
		$this->view();
	}
}

views/top.html

やってること

  • id="spa_content" なdivタグを用意して、この中身を書き換えるようにする
  • JSで、aタグがクリックされたときに以下処理を行うようにしている
    • aタグからhref要素を取得
    • 「#」が含まれていれば以降の処理は無視、1文字目が「/」だったらサイト内リンク、そうでなければ外部リンクと判断(自分の場合はこれで事足りるので雑な実装)
    • 外部リンクだったら別タブで開いて処理終了
    • サイト内リンクだったら「?spa=on」とパラメータをくっつけてgetする(自分の場合は他でクエリストリング使わないので雑な実装)
    • getに成功したらdivタグの中身を書き換えて、ブラウザの履歴も書き換えて、終了
    • getに失敗したらエラーメッセージをポップアップして終了
<!DOCTYPE html>
<html>
<head><title>test</title></head>
<body>

<div>test-header</div>
<div id="spa_content"><?= $content ?></div>
<div><a href="http://google.com">test</a></div>
<div>test-footer</div>

<script src="/jquery.min.js"></script>
<script>
$(document).on('click', 'a', function(){
  href = $(this).attr('href');
  if (href.indexOf('#') != -1){
      return;
  }
  if (href.substr(0, 1) != '/'){
    $(this).attr('target', '_blank');
    return;
  }
  $.ajax(href+'?spa=on',{ type: 'get', }
  )
  .done(function(data) {
    $('#spa_content').html(data);
    history.pushState(null, null, href);
  })
  .fail(function() {
    alert('データの取得に失敗しました。時間をおいて再度お試しください。');
  });
  return false;
});
</script>
</body>
</html>

test_a.html / test_b.html

ただリンク張ってるだけのやつ

<a href="/top/test_b/">test a</a>
<a href="/top/test_a/">test b</a>

…でオッケーかと思ったら足りなかった

ちゃんとしたサンプルはないけど以下のような感じで解決しました。

  • postしたいとき
    • やり方はgetとほぼ同じ。単にpost用の関数を作ってそれをコールするようにした
  • つーかブラウザの「戻る」「進む」が効かない
    • ↓のような感じで解決
// 戻ったり進んだりすると発火するやつ
window.addEventListener("popstate", function($e){
    // aタグクリックで発火するやつの中身をほぼそのまま移植(もちろんpushStateを省く)
}, false);

あとはどうしても「onclick="location.href=..."」したいときとかももちろんSPAしてくれない。

リダイレクトするだけのプログラム

ダウンロード版同人作品の頒布をするとき用のプログラムをざっくり作ってあったので残しておく。

やってることは「特定のURLにアクセスされたら別のURLにリダイレクトする」だけです。
弊サークルでは、「サークル公式ドメインで用意した作品別URL」にアクセスされたら「ダウンロードコンテンツを設置したdropboxの公開ディレクトリ」にリダイレクトするようにしています。

そもそもそういう設置場所URLをダイレクトにバラまいて問題ないならこのプログラムを使う必要はないのですが、
使うと以下のようなメリットがあります。

  • 仮に設置場所を変えても配布URLを変える必要がない(設定を変更するだけでOK)
    • 単に設置場所の整理をしたい場合や、リダイレクト先のURLの方が外部に漏れちゃったので置き場所変えたい…という場合も配布した方のURLは気にしなくて済む
    • 逆に、設置場所は動かないけど自サーバのURLはいつ変わるか分からない…というときは向かないです
  • アクセスログを記録できる
    • 購入してもないのにランダムアタックしてくる奴がいたら分かるようになっています
    • 1つの設置場所に対して複数のURLを作ることもできるので、頒布するイベントや相手ごとにURLを変えれば簡単なマーケティングも可能かも
  • 特定IPからのアクセスを弾くことができる
  • 配布URLを好きな短いものにできる
    • 弊サークルでは「ttp://******.com/dlc/hoge」みたいな感じです。hogeの部分が作品ごとに変わる秘密のコードなやつ。
    • あからさまにdropboxとかのURLをバラまくのもカッコ悪いと感じる場合はそこも解決します

今回必要なかったので実装してませんが、ちょっと改造すれば「有効期限を過ぎたコードは無視する」みたいなこともできますね。

設置・設定方法は末尾に記載。

.htaccess

これは
「このファイルを置いたディレクトリ以下へのアクセスは全部↓の「index.php」にぶっこむよ!」
という設定のファイルです。

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [L]

ただ、.htaccessはレンサバによっては設置できなかったり無視されたりするケースもあります…厄介…
わたしが個人で使っているサーバ(さくら)は大丈夫なんですが、サークルのサーバ(エックス)はだめだったので結局別途対応しています。
サークル主に任せたので方法は把握してませんが、サーバによっては代替の手段があるようです。

index.php

<?php
// config ////////////////////////////////////////////////////////////////////////////

// このファイルを設置しているディレクトリパス(ドメインより後ろ)
$dirpath = "/download/";

// ログファイルパス
$ok_logpath = __DIR__ . "/access_ok.log";
$ng_logpath = __DIR__ . "/access_ng.log";

// IPブラックリスト
$ip_black_list = array(
	"xxx.xxx.xxx.xxx", // ダミー
);

// コードとリダイレクト先の対応
$codelist = array(
	'code_a' => "https://google.com/", // ダミー
	'code_b' => "https://yahoo.co.jp/", // ダミー
);

//////////////////////////////////////////////////////////////////////////////////////

// アクセス元IPを判別(ログ記録・ブラックリスト照合の用途)
// HTTP_X_FORWARDED_FOR と REMOTE_ADDR は前者を優先し、さらにカンマ区切りの場合は最後のものを採用
// 環境によっては上手く取れない可能性もなくはないです。様子見て要調整
$ip = (isset($_SERVER["HTTP_X_FORWARDED_FOR"]) && !empty($_SERVER["HTTP_X_FORWARDED_FOR"])) ? $_SERVER["HTTP_X_FORWARDED_FOR"] : $_SERVER["REMOTE_ADDR"];
if (strpos($ip, ",") !== FALSE)
{
	$ip_list = explode(",", $ip);
	$ip = trim(array_pop($ip_list));
}

// 引数に渡されたコードを判別
preg_match("/^" . preg_quote($dirpath, "/") . "(.*)/", rtrim($_SERVER['REQUEST_URI'], "/"), $match);
$code = empty($match[1]) ? "" : $match[1];

// 【OKパターン】コードが定義されていれば対応するURLにリダイレクト
if (!empty($code) && isset($codelist[$code]) && !in_array($ip, $ip_black_list))
{
	// ログに記録
	if (!empty($ok_logpath))
	{
		file_put_contents($ok_logpath, "[" . $ip . "] " . $code . "\n", FILE_APPEND);
	}

	// リダイレクト
	header("Location: " . $codelist[$code]);
	exit();
}

// 【NGパターン】ログに記録
if (!empty($ng_logpath))
{
	file_put_contents($ng_logpath, "[" . $ip . "] " . $code . "\n", FILE_APPEND);
}

// --------------------------------------------------------------//
// PHP処理はここまで:NGパターンだった場合はそのまま下記HTMLを出力して終了 //
// --------------------------------------------------------------//
?>

<!DOCTYPE html>
<html>
<head>
  <title>ダウンロード</title>
  <meta charset="utf-8">
</head>
<body>
  <div style="text-align:center;">
    <Error!><br>URLが誤っています。<br>お手数ですがもう一度お確かめください。
  </div>
</body>
</html>

設置・設定手順

  • ダウンロードコンテンツdropboxの公開ディレクトリなどに設置する
    • もちろん自分のサーバ内に置くなどでもOK。とにかくアクセスすればダウンロードできるようなURLを作る
  • 上記の.htaccessとindex.phpを任意の公開ディレクトリに設置
  • 必要に応じてindex.phpを調整
    • $dirpath
      • 例えば設置場所が [domain]/download/ ならデフォルトのままでOK
    • $ok_logpath / $ng_logpath
      • アクセスログを取る場合のログファイルパス
      • okが成功時、ngが失敗時
      • ログを取らないなら空文字でOK
    • $ip_black_list
      • IPでブロックする場合のブラックリスト
      • 不審なアクセスがあった場合など、随時ここに追加するとそのIPからのアクセスを無視する
        • エラー等を出すのではなく、正しいコードだった場合も誤っている場合と同じ表示になります
    • $codelist
      • DL用のコードと実際のURLの対応表
      • 例えば「'hoge' => "ttps://google.com/"」という設定であれば、「[domain]/download/hoge」にアクセスすると「ttps://google.com/」にリダイレクトするというわけです
      • コードはパスワードみたいなものなので、「ランダムな長い文字列」にするほど不正なダウンロードを防ぐことができます。多少は構わないと考える場合やそもそも無料配布の場合などは簡単なものにしておいた方がアクセスしてもらいやすくなります
      • コードに使える文字はURL
    • NGパターンのHTML部分のデザインなど

一通り設定・設置してみて、例えば
「/download/」ならアクセスできるのに「/download/test」は404エラーになる!
というような場合は.htaccessが効いていない可能性が高いです。

で、サーバーによってどうしても.htaccess置けない場合はもう「[domain]/download/?c=hoge」みたいなクエリストリング式に改造するかーって感じですね。カッコ悪いけど。

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機能ある時点でそこそこ気持ち悪くない??
そこに権限によっての細かい制御入れるの???
そもそもサイトの仕様をもっとシンプルにしろよって話なの?????教えてエロい人。