カテゴリー: PHP Page 1 of 3

SlackAPIを使って簡単なTODOチェックアプリを作成してみた

WEBエンジニアの石田です。
さて、僕は前回もSlackネタでしたが、今回もSlackネタです。

弊社では、お掃除部という部活(?)がありまして、拭き掃除・ゴミ出し・換気などのオフィス内の掃除、あと朝一のコーヒー作りを有志が毎日行っています。
当番とかは決まっておらず、できるメンバーでやろう!というスタンスなのですが、部員の大半を占めるエンジニア達はフレックス制で出社時間がバラバラ…
となると、チェックリストは欲しいですよね。

紙とかホワイトボードでチェック!となるのがまあ普通だと思いますが、弊社はIT企業。そして社内のコミュニケーションにSlackを使ってるんだから、せっかくならオンラインで済ませてしまいたい。という欲が僕の中で沸々と湧き上がってきたので、SlackAPIを使ってアプリ作りました(・ω・)b

まずは出来上がったものをご紹介します。

f:id:seeds-std:20190425000202g:plain
ボタンをクリックすると名前が登録され、同じボタンを押すと登録が解除されます

仕組み

  • AM9:00になるとタスク一覧と、項目ごとの絵文字のボタンが並んでいるメッセージが自動投稿される。
    • cronで平日毎日9:00にpostするAPIを叩く
  • タスクの絵文字ボタンをクリックすると、クリックしたユーザーのIDがタスク一覧の下に表示される。
  • 既にボタンをクリック済みのユーザーが、もう一度同じ絵文字のボタンをクリックするとタスク一覧の下に表示されていた名前が消える。
    • 絵文字のボタンを押すとSlack から指定したURLにリクエストが送られ、リクエストに応じて処理し、メッセージを書き換えるAPIを叩く

今回つかったもの

  • PHP
  • MySQL
  • Slack API
    • Interactive Components

やってみる

1. Slack APIでAppを作成

  • Interactive Components
    メッセージにボタンやメニューなどを付与し、ユーザーからのアクションに応じて応答できる機能です。
    GitHubやBitBucketのAppをはじめ、最近とても頻繁に使われているので多分見たことあるのではないでしょうか。

https://api.slack.com/apps?new_app=1

f:id:seeds-std:20190610212654p:plain
AppNameは後から変更も可能でした。

上記のURLにアクセスすると、SlackAppの作成画面が出るので AppName (アプリ名) と Development Slack Workspace (使用するSlackのワークスペース) を指定し、 Create App します。
SlackでAPIを作成すると、FeaturesにInteractive Componentsという項目があり、Onにすると設定が可能になります。
f:id:seeds-std:20190610213057p:plain
設定は色々とありそうですが、今回は Request URLに任意のURL( example.com/api/hogehoge.php など)を貼ればOK。

あと、投稿用にSlackのトークンを取得しておきます。
OAuth & Permissions からScopesを絞って Install App。 Permission Scopeはchat:write:botを選択でOKです。

f:id:seeds-std:20190725210251p:plain
権限はchatだけでok
f:id:seeds-std:20190725210349p:plain
Permissionを設定したらInstall App

これでSlack上の設定は完了。

2. サーバー側の設定

とりあえずmysqlでDBを用意。型は適当につけてます

CREATE TABLE polls
(
id          INT AUTO_INCREMENT PRIMARY KEY,
message_ts  VARCHAR(255) NOT NULL,
channel_id  VARCHAR(255) NOT NULL,
text        TEXT         NOT NULL,
answers     TEXT         NOT NULL,
attachments TEXT         NOT NULL,
created_at  TIMESTAMP    NULL
) DEFAULT CHARSET = utf8 AUTO_INCREMENT = 1;
CREATE TABLE votes
(
id          INT AUTO_INCREMENT PRIMARY KEY,
channel_id   VARCHAR(255) NOT NULL,
message_ts   VARCHAR(255) NOT NULL,
user_name    VARCHAR(255) NOT NULL,
user_id      VARCHAR(255) NOT NULL,
action_name  VARCHAR(255) NOT NULL,
action_value VARCHAR(255) NOT NULL,
created_at   TIMESTAMP    NOT NULL
) DEFAULT CHARSET = utf8 AUTO_INCREMENT = 1;

貼り付けた任意のURLではなく、まずpublicの外にでも投稿用のファイルを作成します。
postの第一引数の連想配列が選択肢です。nameに表示する文字を入れて、 iconはslackのアイコンの名前を入力(::はとる)でOK。なくてもOK。

<?php
post([
['name' => '換気',    'icon' => 'wind_blowing_face'],
['name' => 'ゴミ出し',    'icon' => 'wastebasket'],
['name' => '拭き掃除',   'icon' => 'sparkles'],
['name' => 'コーヒー', 'icon' => 'coffee'],
] , '今日のTODO');
function post($questions, $description) {
$attachments = [];
$buttons = [];
$text = $description . "\n";
// answersが表示テキスト、buttonsが実施ボタンになる
$answers = [];
foreach ($questions as $key => $button) {
$answers[] = ':' . $button['icon'] . ': ' . $button['name'];
$buttons[] = [
'name' => $button['icon'] ?? ($key + 1),
'text' => ':' . ($button['icon'] ?? ($key + 1)) . ':',
'type' => 'button',
'value' => $button['name']
];
}
// ボタンは5つを超えるとattachmentがスマホで表示できなくなるためを分割しておく
$block = array_chunk($buttons, 5);
// タスクごとに改行する
$text .= implode("\n", $answers);
// ボタンのブロックごとにattachmentを作成する
foreach ($block as $key => $action) {
$attachments[] =
[
'fallback' => 'daily' . $key,
"callback_id" => "daily",
"color" => "#3AA3E3",
"attachment_type" => "default",
'actions' => $action
];
}
// Slackに投稿を行う
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_URL => 'https://slack.com/api/chat.postMessage',
CURLOPT_HTTPHEADER => [
'Content-Type: application/json; charset=utf-8',
'Authorization: Bearer ' . '【SLACKのトークン】'
],
CURLOPT_POSTFIELDS => json_encode([
'channel' => '【投稿チャンネル】',
'text'    => $text,
'attachments' => $attachments
])
]);
$response = curl_exec($curl);
$response = json_decode($response, true) ?? null;
if ( !$response ) {
return false;
}
//投稿情報をMySQLに保存しておく
$db = new PDO(
'mysql:host=' . '【接続するホスト】' .';dbname=' . '【DB名】' . ';charset=utf8',
'【DBユーザー名】', '【DBパスワード】', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$query = $db->prepare('INSERT INTO polls (message_ts, channel_id, text, answers, attachments, created_at) VALUES (:message_ts, :channel_id, :text, :answers, :attachments, :created_at)');
$query->execute([
':message_ts'  => $response['ts'],
':channel_id'  => $response['channel'],
':text'        => $description,
':answers'     => json_encode($answers),
':attachments' => json_encode($response['message']['attachments']),
':created_at'  => date('Y-m-d H:i:s')
]);
return $response;
}

上記のPHPファイルを実行すると指定したチャンネルに下記のようなフォームみたいなものが投稿されます。

f:id:seeds-std:20190809175228p:plain

各ボタンをクリックすると、先程のURLのhoge.phpにPOSTが走るので、下記を配置しておきます。

<?php
if (!empty($_POST['payload'])) {
$vote = json_decode($_POST['payload'], true);
$message = $vote['message_ts'];
$channel = $vote['channel']['id'];
$user_id = $vote['user']['id'];
$username = $vote['user']['name'];
$db = new PDO(
'mysql:host=' . '【接続するホスト】' .';dbname=' . '【DB名】' . ';charset=utf8',
'【DBユーザー名】', '【DBパスワード】', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$query = $db->prepare('SELECT * FROM polls WHERE message_ts = :message_ts AND channel_id = :channel_id');
$query->execute([
':message_ts'   => $message,
':channel_id'   => $channel
]);
$data = current($query->fetchAll());
if ($data) {
$attachments = json_decode($data['attachments'], true);
//既に存在する実施TODO・実施者IDとボタン押した人の実施TODO・IDが一致したら存在フラグをたてる
$votes = selectVotes($db, $message, $channel);
$exist = false;
foreach ($votes as $answer) {
if ($answer['user_id'] === $user_id && $answer['action_value'] === $vote['actions'][0]['value']) {
$exist = $answer['id'];
}
}
//存在フラグが立ってる場合は削除・ない場合は新規挿入
if ($exist) {
$db->prepare('DELETE FROM votes WHERE id=:id')->execute([':id' => $exist]);
} else {
$db->prepare('
INSERT INTO
votes
(channel_id, message_ts, user_name, user_id, action_name, action_value, created_at)
VALUES
(:channel_id, :message_ts, :user_name, :user_id, :action_name, :action_value, :created_at)')
->execute([
':channel_id'   => $channel,
':message_ts'   => $message,
':user_name'    => $username,
':user_id'      => $user_id,
':action_name'  => $vote['actions'][0]['name'],
':action_value' => $vote['actions'][0]['value'],
':created_at'   => date('Y-m-d H:i:s')
]);
}
// 投稿したTODOの現在の実施者一覧を出す
$votes = selectVotes($db, $message, $channel);
$result = [];
$answers = json_decode($data['answers']);
foreach ($answers as $answer) {
$result[$answer] = '';
foreach ($votes as $vote) {
if ($answer === ':' . $vote['action_name'] . ': ' . $vote['action_value']) {
$result[$answer] .= ' <@' . $vote['user_id'] . '>';
}
}
}
// 実施者一覧をメッセージに反映する
$text = $data['text'];
foreach ($result as $answer => $voters) {
$text .= "\n" . $answer . "\n";
$text .= empty($voters) ? '' : $voters . "\n";
}
$update = [
'channel' => $data['channel_id'],
'ts' => $data['message_ts'],
'text' => $text,
'attachments' => $attachments
];
// Slackのメッセージを更新する
$url = 'https://slack.com/api/chat.update';
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json; charset=utf-8',
'Authorization: Bearer ' . '【SLACKのトークン】'
],
CURLOPT_POSTFIELDS => json_encode($update)
]);
return curl_exec($curl);
}
}
function selectVotes(PDO $db, string $message_ts, string $channel_id)
{
$query = $db->prepare('
SELECT
*
FROM
votes
WHERE
message_ts = :message_ts AND
channel_id = :channel_id
ORDER BY
action_value
');
$query->execute([
':message_ts'   => $message_ts,
':channel_id'   => $channel_id
]);
return $query->fetchAll();
}

送られてきたpostの中にユーザーID・ボタンを押したmessageの情報・押したボタンの情報があるので、それを照合し、あれば削除(MySQLから既存レコードをDELETE)・なければ作成(MySQLにINSERT)し、元のメッセージを状況に合わせて更新することで投票を実現しています。

所感

プログラムの部分で長くはなってしまいましたが、これでTODOアプリは完成です。シーズでは毎日の日次タスクと、月曜日に週次タスクを投稿する形で運用してますが、今のところ問題なく稼働しています。
こういったTODOリストの共有やリマインダ、GitHub/BitBucketの連携など、SlackAPIを応用することで、手軽に様々な機能を日々のコミュニケーションの中に自力で追加できるのはとても魅力的だと感じました。

アイデア次第ではもっと面白いことができそうなSlackAPI、もっと色々な活用方法を見出して社内に還元していきたいなあ…と目論んでおります!

FuelPHP はじめました。複雑な 独自validation 設定には Closure を使おう

Fuel PHP

ども。Webエンジニアの ishino です。

最近 FuelPHP を触る機会が増えてきたのていたので、学んだことを少しづつに記事にしていこうと思います。
珍しく、真面目にプログラムのこと書きましたので、よろしくお願いします。

FuelPHPとは?

FuelPHPはPHPで書かれたオープンソースのHMVCパターンを使うフレームワークです。
フレームワークは基本的な機能が揃っているので、高速にアプリケーションを開発することができます。
PHPのフレームワークにはたくさんの種類がありますが、中でもFuelPHPは比較的新しく、軽量で中規模のアプリケーションの開発に向いていると言われています。

validationの基本

FuelPHPは、POSTされた値の内容に対して、かんたんにvalidationをかけることができます。
必須項目の入力チェックは

$val = Validation::forge();
$val->add('name', '名前')
->add_rule('required');

でOKです。

ただ、複雑なvalidationチェックを行うには、自分で設定しなければなりません。
はじめは、この書き方が分からず、調べましたのでここに書いておきます。

複雑な 独自Validation 設定には Closure を使う

$option['name'] = 'hoge';
$val = Validation::forge();
$val->add('name', '名前')
->add_rule(['validation_name' => function($name) use ($option) {
if ($name != $option['name']) {
Validation::active()->set_message('validation_name', '「:label」が正しくありません');
return false;
} else {
return true;
}
}]);

このようにClosureを使うことで、独自validationを記述できるようになります。

また、functionの引数にはPOSTの値が、
その他でvalidtionで使うパラメータはuseで渡すことも可能です。

また、独自Validationを複数設定する時は、Closureに名前を設定しないとエラーメッセージが上書きされてしまうので、
上のコードのように名前を設定しておくといいですよ。

◎下の記事が参考になりました。
FuelPHPのValidationにクロージャを使う
FuelPHPを更に使ってみて使えるなと思った拡張ValidationRuleの書き方とCore拡張の小技

Codecademyをやってみた

こんにちは、WEBエンジニアのyuchiです。

最近、WEB上でプログラム学習ができる無料のサイトがたくさん作られていますよね。
そんな多々あるサービスの中で、私は以前「Codecademy」というサービス(もちろん無料)を使って勉強してみました。

○Codecademy
http://www.codecademy.com/

普段、私は主にPHPをさわっているので、今回は違う言語にチャレンジしようと思い、
よく使われている汎用なプログラミング言語「Python(パイソン)」を選択してみました。

すると、下記のような画面が出てきます。

 

 

「Codecademy」は日本語に対応していないようなので、説明は英語で書かれています・・・

 

とりあえず説明を読みながら、黒いエディター部分にプログラムを書いて、「Save & SubmitCode」をクリックしました。

 

書いたコードがOKだったら、右上の部分に結果が表示され、プログラムが実行されます。
このように、1つクリアすると次の章に進んでいき、徐々に基礎的なプログラミングが学べるようになっていました。
誰でも気軽に始められるので、初心者の方もこういったツールをぜひ活用してみてほしいです!

以上、終わります。

ISUCON5に「京都スイーツnext」で参加してきました

同僚のishinoと2名でisucon5に参加してきました。

僕はISUCON1の時から参加してるので、今のところ皆勤賞です。
毎年楽しみにしているイベントで今年もすごく楽しかったです。
運営の皆さま、本当にありがとうございました。

結果はスコア、13094。使用言語はPHPです。

しかし本戦出場ならず、、、

「ISUCON5 本選出場者決定のお知らせ」
http://isucon.net/archives/45532743.html

残念。おしかったな~

事前準備

今回、いつも一緒に参加しているスーパープログラマがいなかったので
いつも丸投げしていたアプリ周りの修正に不安を感じていました。
ですので普段業務で一番使っている「PHPを使う」という事にしました。

使った事のなかったGCPの練習もかねて、
Isucon4の問題のイメージが提供されていましたので、起動して
PHP実装を眺めてたりしました。

GCP、すごく使いやすいのですが、インスタンスへのSSHログインにGCP専用のツールみたいなのが必要なのが嫌で
練習中も本番中も ParmitRootLoginはYesだったりパスワード認証許可したりして
ターミナルから直接SSHできるようにしてました。

当日

前日に会社で「キャンプ」が行われて、深夜まで雑談したりで疲れ切ったのちの参加。
ほんと直前までキャンプ場にいましてバタバタしてました。
ですが、アウトドアでIT関連の事から離れた状態でしたので、逆にすっきりした気持ちで参加できたかも。

本戦開始

イメージデータの共有がきたので、ひとまずインスタンスを起動し、
何も触ってない状態でベンチマークをリクエストしました。
が!ベンチが通らない!!(Fail)

え。初期で通らない実装なの?
みたいな感じでプチパニックになりました。

このとき、ベンチからのリクエストが届いてるか、などを確認すればよかったのですが、
GCPの設定がおかしいのか?と無駄に時間を使ってしまいました。

初期で通ってた人もいたようですが、僕らは一度も通らなかったのでいまだに原因ふめい。なんでだろう。
ですが、ログインがめちゃくちゃ重かったり、スコアが80とかから300くらいの人もいたので、
「きっと初期の実装の時点で重すぎてベンチ通らないに違いない」、と結論付けて
PHPへの切り替え作業をしました

systemd

もちろん使った事なかったので、PHPへの切り替えでも時間がかかりました。
PHPのnginx.confは静的ファイルの配信があらかじめnginxからになっててらっくちーん。
もちろんPHPに変えてもいまだベンチは通らない。
でも画面は表示されているのでとりあえず、チューニング開始

チューニング開始

ひとまず、ベンチが通らないと心が不安でいっぱいだったので
簡単に修正できる部分をとりあえずやってみましょう、となりました。

ささっと見てログインの部分でハッシュにしたりしてるのが簡単そうだったので、
パスワード部分に平文を入れちゃってベンチ。
これでようやくスコアが出るようになってた気がします。

ひたすらSQLを書く

・N+1問題
・commentsテーブルにAlterでカラム追加。(30分以上かかった)
・relationsのデータが必ず2つ1組みで入ってたのでselectをoneだけにする
・ORDER BYのcreated_atをidにしてCreated_atのindex消す
・footprintsのgroup byの部分を修正

ほっとんどの時間を/のSQL改修に充てててました。
ミドルウェアの設定はAlterの実行中に設定などを確認、修正をしたくらいです。
(若干my.cnfにははまりました)

結局、ボトルネックmysqlで改修できる部分もまだまだ残したままフィニッシュ
となりました。

全体的に

今回のお題がほんとに楽しく感じました。

・DBのデータが非常に多く複雑なので全メモリ化は実質不可能。
・ログイン処理があり、ログインによって表示される情報が異なるので、フロント側でのキャッシュは難しい(あまりキャッシュヒットしなさそう)
・実際にありがちな「小規模なときはよかったけど、サービスが成長してから問題が出始めた」みたいなリアルな感じ

というところが特に楽しかったです。

あとは例年どうり、新しい事をいろいろ知れてよかったです。
systemdは見ないふりしてたんですが、やっぱり少し触ると、「いいなこれ」ってなりますね。

今年も楽しかったです。
ありがとうございました!

自分の得意なコトをやろう

イギリスの経済学者デヴィッド・リカードは言いました。
「自分の得意なコトをやろう」
(※本当は全然違いますが、ニュアンスはこんな感じです。たぶん。)

チームで仕事をする場合、自分は比較的得意な仕事を担当することが
自分にとってもチーム全体で見てもプラスになります。
なんか当たり前のようなことを言っていますが、よく考えると面白いので、ちょっとお話します。

2つの案件

・案件A:PHP2000行、HTML1200行書く。
・案件B:PHP1000行、HTML500行書く。
この2つの案件を2人でやっつけることを考えます。

パターン1

PHPしかできないプログラマーさんとHTMLしかできないコーダーさん。
この場合、案件A,BのPHP部分をプログラマーさんが、
HTMLはコーダーさんが担当すればいいことがすぐに分かります。
2人が別々に案件A、案件Bをそれぞれ担当しても、片方の作業が全くできないので完遂できません。

パターン2

PHPが得意なプログラマーさんとHTMLが得意なコーダーさん。
今回は、プログラマーさんはHTMLもできます。同じようにコーダーさんもPHPができます。

このケースにおいても、それぞれが別の案件を担当するのでなく、
両案件のPHPプログラマーさん、HTMLをコーダーさんが担当するのが良さそうです。
早く終わって手が空いたらもう片方を手伝う感じですね。

パターン3

最強プログラマーさんと新人プログラマーさん。
お二人共PHPとHTML両方できますが、最強プログラマーさんの方がPHPもHTMLも開発スピードが早いです。
この場合はどう仕事を割り振るのがいいでしょうか?

最強プログラマーさんが1時間あたりPHPのコード100行、HTMLを50行書けるとします。
一方、新人プログラマーさんは1時間あたりPHP50行、HTML20行しか書けません。
とりあえず、それぞれが案件A、案件Bを担当すると

◯最強プログラマーさん
PHP 100行 2000行 20時間
・HTML 50行 1200行 24時間

◯新人プログラマーさん
PHP 50行 1000行 20時間
・HTML 20行 500行 25時間

となりますね。

ところが、これはベストな采配ではありません。
この場合、新人プログラマーさんは自分のより得意なプログラムに専念した方が早く終わります。

具体的には

◯最強プログラマーさん
PHP 100行 750行 7.5時間
・HTML 50行 1700行 34時間
◯新人プログラマーさん
PHP 50行 2250行 45時間
・HTML 20行 0行 0時間

こんな感じです。
新人プログラマーさんの作業量は変わっていませんが、最強プログラマーさんの
作業時間が短縮されました。

新人プログラマーさんのPHP10行はHTML4行の価値しかありませんが、最強プログラマーさんの
PHP10行はHTML5行の価値があります。
なので、HTMLはなるべく最強プログラマーさんに任して新人プログラマーは自分の得意とする
PHPを書いた方が効率がイイのです( ・∀・)イイ!!

もちろん、実際の現場となるとこんな単純な話ではないですが、自分の得意を伸ばすことが
大事だということが分かると思います。

Form::select でvalueが空のoption項目を作る

fuelphpのFormクラスはいろいろと便利です。
その中でも、selectとradioはチェックをつけてくれたりするので大好きです。

Form::select – http://fuelphp.jp/docs/1.7/classes/form.html#/method_select

View内で以下のように書いたら、

[code]
<?
$arrGender = [1 => ‘男性’, 2 => ‘女性’];
$gender = 2;
echo Form::select(‘gender’,$gender,$arrGender,[‘class’ =>’form-control’]);
?>
[/code]

こんなhtmlを作ってくれます。

[code]
<select class=”form-control” name=”gender” id=”form_gender”>
<option value=”1″>男性</option<
<option value=”2″ selected=”selected”>女性</option>
</select>
[/code]

通常は$arrGenderの部分は別で定義しておきますし、
$genderの部分はModelあたりから取ってきたりPostされた値が入っています。

$arrGenderの配列に合わせてoption の部分を作ってくれて、
$genderのキーと一致するoptionにselectedを付けてくれます。超便利。

ですが、これだとユーザーが本当に値を選択したのかわからないので、入力が空ならエラーを出す形にしたい。
つまりこういうHTMLを出したい。

[code]
<select class=”form-control” name=”gender” id=”form_gender”>
<option value=””>選択してください</option>
<option value=”1″>男性</option>
<option value=”2″>女性</option>
</select>
[/code]

$arrGenderをこんな配列にしてやれば実現できました。

[code]
<?
$gender = null;
$arrGender = [” => ‘選択してください’, 1 => ‘男性’, 2 => ‘女性’];
echo Form::select(‘gender’,$gender,$arrGender,[‘class’ => ‘form-control’]);
?>
[/code]

PHP のオブジェクトの比較について

こんにちは kinu です。PHP のオブジェクトを比較したいときに、
はたして比較用の演算子がそのまま使えるのか?と思い調べました。

対象を用意する

まずテスト用にクラスを定義します。

[code]
class Test
{
private $a;

private $b;
public function __construct($a, $b)
{
$this-&gt;a = $a;
$this-&gt;b = $b;
}

}
[/code]

このクラスのインスタンスに対して検証していきたいと思います。

オブジェクトに比較の演算子が使えるのか

以下のコードを実行できるか試してみます。

[code]
$obj1 = new Test(1, 1);
$obj2 = new Test(1, 2);
if ($obj1 == $obj2) {
echo '同じ'."\n";
}
if ($obj1 &lt; $obj2) {
echo &#039;小さい&#039;.&quot;\n&quot;;
}
[/code]

結果

小さい

エラーは出ず、それっぽく動きました。

どのようにして比較されるのか

では実際にどういう判定をしているのか以下のコードでプロパティを数値のみにして検証してみました。
PHPのバージョンは5.6.4です。

[code]
function compare($obj1, $obj2) {
$result = array();

    if ($obj1 === $obj2) {
$result[] = 'まったく同じ';
}
if ($obj1 == $obj2) {
$result[] = '同じ';
}
if ($obj1  $obj2) {
$result[] = '大きい';
}
return $result;

}

function check($sets, $expect)
{
foreach ($sets as $set) {
$obj1 = new Test($set[0][0], $set[0][1]);
if ($set[1][0] == ‘clone’) {
$obj2 = clone $obj1;
} else {
$obj2 = new Test($set[1][0], $set[1][1]);
}

      $result = compare($obj1, $obj2);
if ($expect != $result) {
echo '(' . join(',', $set[0]) . '), (' . join(',', $set[1]) . '): ';
echo join(',', $expect) . ' -&gt; ' . join(',', $result)."\n";
}
}

}

//同じになりそうな組み合わせ
$same = [
[[1, 1], [1, 1]],
[[1, 1], [‘clone’]],
[[1, 2], [2, 1]],
[[2, 1], [1, 2]],
[[2, 2], [2, 2]],
];

// $obj1 が大きくなりそうな組み合わせ
$big = [
[[1, 2], [1, 1]],
[[2, 1], [1, 1]],
[[2, 2], [1, 1]],
[[2, 2], [2, 1]],
[[2, 2], [1, 2]],
];

// $obj1 が小さくなるような組み合わせ
$small = [
[[1, 1], [1, 2]],
[[1, 1], [2, 1]],
[[1, 1], [2, 2]],
[[1, 2], [2, 2]],
[[2, 1], [2, 2]],
];

check($same, array(‘同じ’));
check($big, array(‘大きい’));
check($small, array(‘小さい’));
[/code]

オブジェクトのプロパティの合計値で比較してるのかなと思い上記のようなデータで結果を予想しました。
このプログラムは予想と違っていた場合にその組み合わせと結果を出力します。

結果

[code]
(1,2), (2,1): 同じ -> 小さい
(2,1), (1,2): 同じ -> 大きい
[/code]

どうやら予想とは違うみたいです。組み合わせの数値と結果をみてみると、プロパティの順番が重要そうです。
予想と違った組み合わせでいえば一つ目のプロパティを比較した結果と同じになっています。
その他の組み合わせも一つ目が同じなら次のプロパティを比較して結果を出してると考えるとつじつまが合います。
試しに次の組み合わせもしてみました。

[code]
$same = [
[[2, 0], [1, 1]],
];
$small = [
[[2, 1], [1, 3]],
];
check($same, array(‘同じ’));
check($small, array(‘小さい’));
[/code]

結果

[code]
(2,0), (1,1): 同じ -> 大きい
(2,1), (1,3): 小さい -> 大きい
[/code]

最初の予想は完全に間違いであることがわかりました。
またこの結果もひとつめのプロパティの比較と同じになってます。

まとめ

文字列を値にもったプロパティについての検証がないなど中途半端な検証になってしまいましたが、
おそらくオブジェクトを比較すると最初に同値にならなかったプロパティの比較の結果(ない場合は同値)になる
ということで問題ないんじゃないかなと思います。
公式のドキュメントのUser Contributed Notesにもありましたし…。
ということでPHPでオブジェクトを比較したいときは比較用のメソッドを作って使いましょう。

参考

http://php.net/manual/ja/language.oop5.object-comparison.php

例外はきちんと書こう

PHPで、とあるリクエストに対して、xml形式でレスポンスを返すというプログラムを作成していました。

メインPHP

main.php
function main() {
// 1.DBより何か参照してくる
$rs = $child-&gt;reference();
// 2.domDocumentを利用し、xml生成する
$dom = new domDocument('1.0', 'UTF-8');
・・・省略・・・
// 3.レスポンス返す
header('Content-Type: application/xhtml+xml');
echo $dom-&gt;saveXML();
}

サブPHP

child.php
function reference() {
try {
// selectして結果返す
$rs = $dao-&gt;select('table', $columns, $where);
return $rs;
} catch (Exception $e) {
throw $e;
}
}

が期待した動作をせず、、、

Empty reply from server

みたいなエラーが返されてます。空がサーバから返されてる。。!?

何気にapacheのログを参照すると、、

child pid xxxx exit signal Segmentation fault (11)

なるエラーが!?
ググってみるとふむふむ、うーん、参照してはいけないメモリ領域を参照してるみたいで詳細を調査する場合は、コアダンプをはいてみないといけない・・・はい、レベル高いです!!
まだロジックで調べてみることがあるだろうと、気持ちを入れなおし、ごにょごにょ調べていると。。おおっ!SQLがエラーログはいてました!!

2以降の処理は、1からの正常処理しか期待していないロジックを書いていました。
きちんと例外(エラー)のことを考えないといけないですね。
メインPHPを以下のように改善しました。

改善後メインPHP

main.php
function reference() {
// 1.DBより何か参照してくる
$isSuccess = true;
try {
$rs = $child-&gt;reference();
} catch (Exception $e) {
$isSuccess = false;
}
// 2.domDocumentを利用し、xml生成する
$dom = new domDocument('1.0', 'UTF-8');
if ($isSuccess) {
// 2-1.正常処理
・・・省略・・・
} else {
// 2-2.例外処理
・・・省略・・・
}
// 3.レスポンス返す
header('Content-Type: application/xhtml+xml');
echo $dom-&gt;saveXML();
}

エラーでなくなりました。よかったです!
SQLエラーは仕方ないとして、どうしてSegmentation faultが出力されるのでしょうかね。
XML(DOM?)オブジェクトあたりが怪しい??詳細は不明、、、

例外処理もちゃんと考慮して書くようにしましょう。

fuelphpでcsvファイルをアップロードして読み込み処理

こんにちは

WEBエンジニアのyuchiです。

前々回の記事に引き続き、開発でfuelphpからCSVファイルをアップロードして読み込みしないといけなかったので、
その時調べた方法を書きたいと思います。

まずはCSVファイルをアップロードします。

// 初期設定
$config = array(
'path' =&gt; dirname(DOCROOT).'/uploads/',
'randomize' =&gt; true,
'ext_whitelist' =&gt; array('csv'),
);
// アップロード基本プロセス実行
Upload::process($config);
// 検証
if (Upload::is_valid())
{
// 設定を元に保存
Upload::save();
// 情報をデータベースに保存する場合
$result = Model_Uploads::deliv_add(Upload::get_files());
}

configでアップロードする場所をpathに指定し、保存するファイルの拡張しを設定し保存します。
randomizeをtrueにすると保存されるファイル名がランダムにつけられます。

次にアップロードしたファイルからcsvの読み込みを行います。

foreach ($files as $file)
{
$data = file_get_contents($file['saved_to'].$file['saved_as']);
$data = mb_convert_encoding($data, 'UTF-8', 'SJIS');
$data = Format::forge($data, 'csv')-&gt;to_array();
}

csvファイルの文字コードが分からないため、文字化け防止としてmb_convert_encodingで変換します。
例としまして、ここではSJISからUTF-8に変換。

あとは、読み込んだcsvファイルのデータを表示したりするだけです。

foreach($data as $list)
{
echo $list;
}

fuelphpの機能を使えば簡単にできました。

ちなみにアップロードしたファイルを削除したい場合は、

//ファイルを削除したい場合
File::delete(dirname(DOCROOT).'/uploads/' . ファイル名);
//ディレクトリを削除したい場合
File::delete_dir(dirname(DOCROOT).'/uploads/');

を書けばOKです。

では、今日はこのあたりで!

三山崩しゲーム

名前だけ聞いても分からないかもですが、やったことのある方もたくさんいると思います。
私も授業中とかによくやりました。
最後の1つを取ったら勝ち的なゲームです。
このゲームには必勝法があるので、今回はそれを紹介します。

ルール

  1. いくつかの石を3つの山に分けます。
  2. 2人のプレイヤーは交互に1つの山から1つ以上の好きな数の石をとる。
  3. 最後の1つを取った方の勝ちヽ(´∀`。)ノ゚イェイ

必勝法

説明がちょっと難しいのですが、
それぞれの山の石の数を2進法展開して同じ基底の係数の和が全て偶数になるように石をとります。
文章で書かれてもよく分からないと思うので、PHPのコードにしてみました。

function saikyou($input) {
$order = ceil(log(max($input), 2)) + 1;
arsort($input);
foreach ($input as $k =&gt; $v) {
$binary[$k] = str_split(str_pad(decbin($v), $order, "0", STR_PAD_LEFT));
}
$key = null;
for ($i = 0; $i &lt; $order; $i++) {
$sum = 0;
foreach ($binary as $v) {
$sum += (int)$v[$i];
}
if ($sum % 2 == 1) {
if ($key === null) {
foreach ($binary as $k =&gt; $v) {
if ($v[$i] === "1") {
$key = $k;
$binary[$key][$i] = "0";
break;
}
}
} else {
$binary[$key][$i] = (string)(1 - (int)$binary[$key][$i]);
}
}
}
ksort($binary);
foreach ($binary as $k =&gt; $v) {
$output[$k] = bindec(implode("", $v));
}
return $output;
}

$inputには配列でそれぞれの山の石の数を入れます。
↓こんな感じになります。

$input  = array(9, 2, 5);
$output = saikyou($input);  // array(7, 2, 5)

$outputは最強の石の取り方でできたそれぞれの山の石の数です。
上の例では(7,2,5)なので、9の山から石を2つ取るのが最強です。
ちゃんと2進法展開の同じ基底の係数の和が全て偶数になっているのが分かります。
山の数は3つでなくても同じですし、石の数もいくつあっても考え方は一緒です。

便利な関数がたくさんあって短いコードでスッキリ書けました。
説明の難しいこともコードにするとスッキリですね。

◯参考
三山崩しゲーム

Page 1 of 3

© SEEDS Co.,Ltd.