PHP に関する投稿を表示しています

PHP で本文抽出したいよね、という

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

前回の記事: ExtractContent を PHP で書き換えた | ごみばこいん Blog

「今回 PHP に書き換えてみた ExtractContent も含めて、これらの比較は次の記事でやりたい」といいつつ、他のネタが挟まり、ようやくにして比較しました。

比較した結論

お試しにはこちらのリポジトリをどうぞ。
sters/compare-article-extractors: Compare web article extractors.

日本語環境下で難しい話

php-goose 、または php-web-article-extractor では、どちらも内部にストップワード辞書を持っていて、プログラム上で言語を指定、あるいは lang 属性や meta タグから読み取った言語名から、適切なストップワード辞書を選択しています。そうして決まったストップワードの一覧が、文字列にどれくらいあるかを確認して、本文かどうか?といった判断を進めているようです。

ストップワードとはよく使われがちな単語のことで、自然言語処理の前処理として行なうことでデータ量を減らしたり、精度を上げたり出来ます。

自然言語処理における前処理の種類とその威力 - Qiita

例えば英語では「This is a pen.」といったようにスペースで区切られているので「ストップワードの is と a の 2 文字がある!こいつはコンテンツだ!」と簡単に出来るのですが、日本語の場合はそう簡単な話ではありません。日本語はスペース区切りではありませんし、前後にある文字によって、まったく異なる意味合いになったりもしますので、一概に辞書でドバーッと指定することは適切な処理でない可能性が高いです。

じゃあどうするかというと分かち書きの出番なのですが、分かち書きも簡単なものではありません。 Chasen や Mecab といった既存の技術を利用すればお手軽にもできますが php-goose や php-web-article-extractor がそれを利用して日本語対応するかというとちょっと違いそうだなあ、という気持ちです。

php-goose に出したプルリクは諦めてドバーッと処理する形ではありますが、日本語だけ特殊化せざるを得ないので、そうやってみて出してみたものの、なんだかなあと。いやそもそも 元になってる Goose はそういう実装になっていないだろうし…。

そのほかのアプローチ方法

DOM構造や、画面上の位置情報も利用するのはめっちゃ有効的だと思います。
PuppeteerでWebページからメインコンテンツっぽいところを抽出してみる - Qiita

上記の記事の「まとめ」でも出てはいますが、機械学習して取るようにするのもよさそうです。
seomoz/dragnet: Just the facts -- web page content extraction

仕事スクレイピングを楽にした

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

この記事は Webスクレイピング Advent Calendar 2017 - Adventar の 5 日目の記事です。

お仕事都合による発生するスクレイピングについてと、それつらたん~~~なことになってきたので解消するようにブチアゲした話を書きます。

お仕事都合による発生するスクレイピング

お仕事都合、例えば営業や企画といった人達から「こういうこと出来ないかなあ…?」とかって話が来るとおよそ7,8割の確立でスクレイピングできないっすかというお願いです。それが 1 回だけならいいんですが、異なる人から、異なることを、ちょこちょことお願いが来たりします。
さらに厄介なのが「XX さんに先月お願いしたやつ、今月もよろしく」みたいなやつ。ぼくは XX さんじゃないし、その事があったことを知らないぞ??

そしてその "お願い" の中身としては、例えばこういう感じ。

  • 指定する URL 群から XXX と YYY の情報を集めたい
    • (これがいちばんおおい)
  • 電車案内のサイトから駅名一覧を集めたい
  • 求人系のサイトから職種一覧、募集企業の情報を集めたい
  • IRまとめ系サイトから業種や会社名、連絡先を集めたい
  • 特定のメディアサイトに入っている画像を集めたい
  • 〇〇のメーカーの商品、型番の一覧を集めたい
  • ムフフなコンテンツ集めたい
  • etc...

そしてこういったお願いはかなりの速度感を求められることがあります。

とはいえ、どばーーーーってスクレイピングしたら、対象のサイトに対して、迷惑を掛けることにつながりますし、訴訟リスクも存在します。前提として 1 日目の vaaaaanquish さんが書かれているようなこと( PythonでWebスクレイピングする際の規約回りの読み込み - Stimulator ) を
確認しつつ、良心的な範囲でやらなければいけないので、その点については説明し納得をしてもらっています。

そんなこんなで、これまではまあいっかーという気持ちで、毎回 0 からコードを書いて、ぐるぐるしたりしてやっているのですが、いやいやちょっとそろそろ面倒になってきましたよ、ということでもうちょっとやることにしました。

「コレ使えばおおよそいいよ」セットを作る

毎回コードを書くのはまあいいとして、でも毎回 0 スタートはかなり面倒です。ボイラープレートというか、フレームワークというか、何かそういう「コレ使えばおおよそいいよ」セットを作ります。
このセットがあることによって、コードがざっくりと統一されるので、引き継ぎだったり、並行開発だったり(するか?)、そのあたりの勝手が良くなります(なりました)。

Guzzel 使えばいいじゃん、とかそういう話でもあるんですが、スクレイピングしたときってデータの入出力はもちろん、スクレイピングのやり方によってはキューイングしたり、一時データをもったりするじゃないですか。そういうところ全部扱いたいんですよ。で、後述するんですけど、速度感よくやっていこうとすると、あーだこーだ composer require するってよりは、ファイル 1 個読めばいいじゃん、という環境にしたいのです。

制約とか

お仕事の都合によって、言語の制約は PHP になります。他の言語でもいいのですが、周りの人の取り回しを考えるとこれがベストマッチです。

また composer や PEAR は利用できません。お願いを解決するにあたって速度感よくやるために「コレ使えばおおよそいいよ」セットは、何も考えずにとにかくサクッと使えることが必要です。なんなら PHP の設定も気にせずに使えるように、どこかのサーバにファイルを設置して eval(file_get_contents()) みたいに使えると最高です。

そうして出来たものがこちらです

easy scraping kit
※ CSS セレクタを扱う箇所がざっくり過ぎて正しくないです

コレを使って、例えば「Mapion を利用して、指定する都道府県・市区町村の駅名を集める」ものを書くとこんな用になります。

// プログラム本体
eval(file_get_contents('https://.../scraping_kit.txt'));

HTMLDoc::$waitMin = 5000;
HTMLDoc::$waitMax = 15000;

$baseUrl = 'https://www.mapion.co.jp';

foreach(Console::getListFile('list.csv') as $zip) {
	Console::out("start {$zip[0]}");

	// search pref
	$topDoc = HTMLDoc::loadURL($baseUrl . '/station/');
	$prefLinks = $topDoc->findCSSPath('.section.type-a a');
	foreach($prefLinks as $prefLink) {
		if ($prefLink->textContent !== $zip[0]) {
			continue;
		}
		Console::out("found {$zip[0]}");
		Console::out("start {$zip[1]}");

		// search city
		$prefDoc = HTMLDoc::loadURL($baseUrl . $prefLink->attributes['href']->value);
		$cityLinks = $prefDoc->findCSSPath('.section.type-a a');
		foreach($cityLinks as $cityLink) {
			if (mb_strpos($cityLink->textContent, $zip[1]) === false) {
				continue;
			}

			Console::out("found {$zip[1]}");

			// search station
			$cityDoc = HTMLDoc::loadURL($baseUrl . $cityLink->attributes['href']->value);
			$h1 = $cityDoc->findCSSPath('h1.type-a-ttl');
			$h1 = explode('の', $h1[0]->textContent);
			$h1 = $h1[0];

			$trainLinks = $cityDoc->findCSSPath('table.list-table tr');
			foreach($trainLinks as $train) {
				$item = [
					$h1,
				];
				$tds = $cityDoc->findCSSPath('td', $train);
				foreach($tds as $td) {
					$item[] = str_replace('[MAP]', '', trim($td->textContent));
				}
				Console::outputResult($item);
			}
		}

		Console::out("done {$zip[0]}{$zip[1]}");
	}
}
// list.csv の例
神奈川県,川崎市
神奈川県,横浜市

今ままで 0 から書いて、1,2 時間かかってコードを書いていたのがサクッと終わるようになりました。
また、簡単にではありますが、キューやキャッシュも持てるようになるので、 cron などを使って定期的に回したり、 web サイト A の情報を元に web サイト B の情報を取得するといった複数のステップがあるようなスクレイピングもわりかし簡単にできるようになりました。

ごく簡単なスクレイピングはコードを書かずに済ませたい

「コレ使えばおおよそいいよ」が出来ましたが、そもそもとして、よく発生するお願いは「指定する URL から XXX と YYY の情報を集めたい」です。これってコードを書く必要はほとんどないはずで、入力として URL と CSS セレクタ( もしくは XPath セレクタ)さえあれば、あとはよろしく処理するような仕組みがあれば十分なはずです。

というわけで一旦雑に作って様子を見ています

11 月くらいに作って様子を見ようと思っていたのですが、特に何かあったわけでもないんですが、急にお願いが減っちゃって作らなくていいかなあと思ってきました。とはいえ怠けているとあとが大変になりそうな予感がするので、近いうちにやりたい気持ち。

おわりに

「コレ使えばおおよそいいよ」を作ってお仕事都合のスクレイピングを簡単にしたり、別の開発者が入るときも問題の起きにくい状況になりました。
しかし、もはやこの件についてコードを書くことをなるべくやめたいので、そういう「スクレイピングいい感じにできるツール」みたいなのあったら教えてください。

(Google スプレッドシートで出来ることは知っていますが、時間系のあたりをもうちょっと自由度が効く感じで…)


6 日目は _kjou さんで「何か書きたい」です。こちらもお楽しみに!

Webスクレイピング Advent Calendar 2017 - Adventar

前後の記事

Next:
Prev:

ExtractContent を PHP で書き換えた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

いや Ruby でええやん、 Python でええやん、みたいな話だとは思うのだが、やっぱり PHP でやりたいよね、という一定の需要がギョームで発生してしまったので、 Ruby のコードを見ながら PHP に書き換えた。 Packagist にも登録してあるので composer からどうぞ。

sters/extract-content - Packagist

$url = 'http://labs.cybozu.co.jp/blog/nakatani/2007/09/web_1.html';
$extractor = new \ExtractContent\ExtractContent(file_get_contents($url));
$result = $extractor->analyse();
file_put_contents(__DIR__ . '/result', $result);

// // 抽出結果
// Webページの自動カテゴライズ の続き。
// 前回書いたとおり、パストラックで行っている Web ページのカテゴライズでは、Web ページの本文抽出がひとつの鍵になっています。今回はその本文抽出モジュールを公開しつつ、使っている技法をざっくり解説などしてみます。
// 本モジュールの利用は至極簡単。require して analyse メソッドに解析したい html を与えるだけ。文字コードは UTF-8 です。
// ...

ExtractContent は Cybozu の Nakatani Shuyo さんが2009年に作成したもので、正規表現を主として Web 記事上の本文に関するちょっとの知見が加わることで成り立っている。以下の記事へ。

Webページの本文抽出 (nakatani @ cybozu labs)

今回、移す際に参考にしたのは、もともとのものではなく、Ruby 1.9 に対応したソース。というのもこっちの記事を見つけたのは後で、 Github 上で ExtractContent を先に見つけていたので、まあいっか、と。

mono0x/extractcontent: ExtractContent for Ruby 1.9+

Ruby はチョットヨメルので inject とかわからないメソッドだけドキュメント見つつ脳内補完して、同じような感じの処理になるよう PHP へ書き直した。工夫したところはとくになく、クラスで扱うようにした、チョットテスト書いたくらいで、ほぼそのまま移してきた。Wikipedia と Medium、はてなブログあたりで試してみたところで、おおよそうまくいっているように見えたので、たぶん大丈夫。

ただ、試していて、本文っぽいと判断されるのが 2 つ以上あるような 1 記事ページ(例えば記事中に section タグがあってーとか、そいういう)ではうまく取り出せず、スコアが高くなったほうのみ抽出されてしまう。本文しきい値のようなものを設けてそれを超えていたら、結合して出す、とかしないといけないなあと思う。とはいえ、正確に本文が欲しいのか、その記事中の重要な部分にフォーカスするのか、などなど要件にもよるので、とりあえずはいいんじゃないかなの気持ち。

ちなみに PHP による実装もいたのだが、おそらく上記のようなところで、元々のものには無いオプションが増えていたりでチョットわからなかったので、一から書いた次第。

aoiaoi/ExtractContent: extract content from HTML

なお記事解析、本文抽出について Packagist を調べると他にも 3 つのライブラリが出てくる。ざっくり紹介するとこんな感じ。

今回 PHP に書き換えてみた ExtractContent も含めて、これらの比較は次の記事でやりたい。

前後の記事

Next:
Prev:

faker を使ってダミーデータを生成する

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

これこれ、このライブラリ。

fzaninotto/Faker: Faker is a PHP library that generates fake data for you

ざっくり翻訳しつつ、日本語データでの使い方へ。

faker とは

faker は PHP のライブラリで、偽のデータを提供する。データベースの初期化や XML ドキュメントの生成、ストレステスト、本番データの匿名化、などに活用できる。faker は Perl の Data::Faker と Ruby の Faker に影響を受けている。

faker の使い方

インストール方法

Pakagist にて提供されているので composer を使ってインストールできる。

composer require fzaninotto/faker

基本的な使い方

$faker = \Faker\Factory::create();
// faker オブジェクトの生成

echo $faker->name;
// 名前。例えば 'Lucy Cechtelar'

echo $faker->address;
// 住所。例えば
// "426 Jordy Lodge
// Cartwrightshire, SC 88120-6700"

echo $faker->text;
// 文章…というよりは Lorem 。例えば。
// Dolores sit sint laboriosam dolorem culpa et autem. Beatae nam sunt fugit
// et sit et mollitia sed.
// Fuga deserunt tempora facere magni omnis. Omnis quia temporibus laudantium
// sit minima sint.

faker には嬉しいことに、日本語を含む色々な言語のダミーデータをサポートしている。ここでは日本語での扱いを試してみる。

// 引数に言語を指定すると利用してくれる
$faker = \Faker\Factory::create('ja_JP');

echo $faker->name;
// 例えば '大垣 直子'

echo $faker->address;
// 例えば '2636573  滋賀県浜田市西区笹田町渡辺1-1-10'

echo $faker->realText;
// text は Lorem なので realText がある
// 例えば 'オン燈と、もうすっかりがきの音にすきっと思いなやさで伝つたわ」「なんだ」カムパネルラはこち見ていたの...'

どういうプロパティが用意されているか

英語プロパティは README に全てあって、n - m の範囲で乱数とか、「Dr.」などの敬称を使えるとか、日時とか、まあなんか色々とあるのでそっちを見ると良さそう。

Faker/readme.md at master · fzaninotto/Faker

日本語のだけ、ソースとにらめっこしないと分からないところがあったので、取り上げる。(網羅できてないかも)

country 国名
prefecture 都道府県
city
ward
streetAddress 町以下
postcode 郵便番号
secondaryAddress マンション名
company 会社名
userName ユーザ名
domainName ドメイン名
email メールアドレス
name 名前(姓 + 名)
lastName
firstName
firstNameMale 名(男性)
firstNameFeMale 名(女性)
kanaName 名前カナ
lastKanaName 姓カナ
firstKanaName 名カナ
firstKanaNameMale 名カナ(男性)
firstKanaNameFemale 名カナ(女性)
phoneNumber 電話番号
realText 日本語文章

ORMとの連携

faker は CakePHP や Laravel の ORM と連携することができる(!?)
試してみたところ、連携というよりは ORM を通して、実際に DB に値を格納してくれるっぽい。

例えば CakePHP3 で使うならこんなふうに。

$faker = \Faker\Factory::create('ja_JP');
$populator = new \Faker\ORM\CakePHP\Populator($faker);
$populator->addEntity('Users', 5, [
    'name'       => function() use ($faker) { return $faker->name; },
    'prefecture' => function() use ($faker) { return $faker->prefecture; },
    'created'    => null,
    'modified'   => null,
]);
$inserted = $populator->execute();

$Users = \Cake\ORM\TableRegistry::get('Users');
debug($Users->find('all')->toList());
// 例としてこのような出力になる
// [
//     (int) 0 => object(App\Model\Entity\User) {
//         'id' => (int) 1,
//         'name' => '津田 裕樹',
//         'prefecture' => '栃木県',
//         ....
//         'created' => object(Cake\I18n\FrozenTime) {
//             'time' => '2017-11-20T13:47:32+09:00',
//             'timezone' => 'Asia/Tokyo',
//             'fixedNowTime' => false
//         },
//         'modified' => object(Cake\I18n\FrozenTime) {
//             'time' => '2017-11-20T13:47:32+09:00',
//             'timezone' => 'Asia/Tokyo',
//             'fixedNowTime' => false
//         },
//     },
//     ...
// ] 

特に指定をしなかったカラムについては DB の形を見て、結構いい感じに適当に faker な値で埋めてくれる。指定はクロージャで、 faker を使ってランダムなデータを作ることも出来るし、固定値にすることもできる。また、例としては 1 エンティティしか作らなかったが、addEntity を複数呼んで、 まとめて execute することで複数のエンティティも作ることができる。

シード値を設定する

テストで使うときやっぱりランダムだとつらいよね、というときのためにシード値を設定することができる。

$faker = \Faker\Factory::create();
$faker->seed(1234);

echo $faker->name;
// 何度コードを流しても以下の順番で流れる
// 'Miss Lorna Dibbert'
// 'Litzy Emard'
// 'Odessa Collins'
// ...

ランダムに提供される部分はそのとおりなのだが、日付については引数を指定しないと now() な値が使われるのでシードは同じでも出力される値が変わってしまうので、なんでもいいが毎回同じ値を入れる必要がある。

$faker = \Faker\Factory::create();
$faker->seed(1234);
echo $faker->dateTime();
// ランダムになる

$faker = \Faker\Factory::create();
$faker->seed(1234);
echo $faker->dateTime('2017/01/01 12:00:00');
// 固定パターンになる

faker には乱数を提供する関数もあり、シード値を設定することで、こういったものたちも固定パターン化される。便利ちゃん。

$faker = \Faker\Factory::create();
$faker->seed(1234);
echo $faker->numberBetween(0,100);
// 固定パターンになる
// 81, 50, 62, 18, 56, ...

faker の内部、プロバイダの話

何の気無しに \Faker\Factory::create() を呼び出していたが、実は内部でこんなような処理をしている。

$faker = new \Faker\Generator();
$faker->addProvider(new \Faker\Provider\en_US\Person($faker));
$faker->addProvider(new \Faker\Provider\en_US\Address($faker));
$faker->addProvider(new \Faker\Provider\en_US\PhoneNumber($faker));
$faker->addProvider(new \Faker\Provider\en_US\Company($faker));
$faker->addProvider(new \Faker\Provider\Lorem($faker));
$faker->addProvider(new \Faker\Provider\Internet($faker));

※実際のコードはこのあたり。
Faker/Factory.php at master · fzaninotto/Faker

プロバイダの土台として \Faker\Provider\Base がいるので、これを継承した適当なクラスを作り、 public なメソッドを作って return すれば良い。そのプロバイダクラスを new して addProvider すれば使えるようになる。

class Book extends \Faker\Provider\Base
{
  public function title($nbWords = 5)
  {
    $sentence = $this->generator->sentence($nbWords);
    return substr($sentence, 0, strlen($sentence) - 1);
  }

  public function ISBN()
  {
    return $this->generator->ean13();
  }
}


$faker->addProvider(new Book($faker));

echo $faker->ISBN;

faker を使ってみて

説明にもあるが、テストコードにおいて、いや、そこはなんでも良いんだけど、それっぽい住所や名前をよろしく入力して欲しい、みたいな時。あるいは、DBの初期化時に、多いに力を発揮しそうな感触。

テストコードとして利用する場合、値が毎回ランダムになって使い所がイマイチわかりにくい気もちょっとするが、それで詰まるときってそんなに無いのでは…。値が被って~~~うあああ!!!!みたいなことはあると思う。

話はそれるが最近見かけた property-based testing という方法にはめちゃんこ合っていると思う。

Property-Based Testing for Godly Tests

そもそも PHPUnit なら Data Provider の仕組みがあるし、あるいは CakePHP なら Fixture が、 Laravel には Factory の仕組みがそれぞれにある(きっと他のフレームワークにもあるんじゃないかな、調べてない。。)ので、そういうこともやりやすいと思う。
ちなみに Laravel には faker が既に含まれていて、 Factory の仕組みを使うとガンガンにテストデータを作ってくれる。スゴイ。

Database Testing - Laravel - The PHP Framework For Web Artisans

ORMとの連携はできるとはいえ、既に書いたとおり、各フレームワークがそういう仕組みを提供しているので、それとバッティングするなあという気持ちも。その中で使っていく、みたいなイメージなのかな。。ちょっとわからないや。。

前後の記事

Next:
Prev:

CakePHP3 で論理削除をする

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

CakePHP3 ではデフォルトでは論理削除の機能は実装されていない。調べると1番上にいい感じに利用できるプラグインがあるので、これを利用するとよい。

PGBI/cakephp3-soft-delete

とりあえず PGBI/cakephp3-soft-delete を使ってみる

composer でパッケージを追加して。

$ composer require pgbi/cakephp3-soft-delete "~1.0"

プラグインの読み込みを追加して。

// config/bootstrap.php
Plugin::load('SoftDelete');

テーブルで SoftDeleteTrait を読み込んで。

// src/Model/Table/UserTable.php

...

use SoftDelete\Model\Table\SoftDeleteTrait;

class UsersTable extends Table
{
    use SoftDeleteTrait;
    ...

該当のテーブルに deleted というカラムを追加して。

// config/Migrations/20171114120000_CreateUsers.php
...

$table->addColumn('deleted', 'datetime', [
    'default' => null,
    'null' => true,
]);

...

Enjoy!

// コンソール機能を使って動作確認をしてみる
//==============================
$ bin/cake console


// Table の読み込み
//==============================
>>> use Cake\ORM\TableRegistry;
>>> $Users = TableRegistry::get('Users');
=> App\Model\Table\UsersTable {#214
     +"registryAlias": "Users",
     +"table": "users",
     +"alias": "Users",
     +"entityClass": "App\Model\Entity\User",
     +"associations": [],
     +"behaviors": [
       "Timestamp",
     ],
     +"defaultConnection": "default",
     +"connectionName": "default",
   }


// Entity の作成
//==============================
>>> $entity = $Users->newEntity();
=> App\Model\Entity\User {#216
     +"[new]": true,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// データの保存
//==============================
>>> $Users->save($entity);
=> App\Model\Entity\User {#216
     +"created": Cake\I18n\Time {#232
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\Time {#218
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"id": 2,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// データ保存の確認
//==============================
>>> $Users->get(2);
=> App\Model\Entity\User {#295
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#286
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#285
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// SoftDeleteTrait を読み込むと delete の挙動が SoftDelete に変わる
//==============================
>>> $Users->delete($entity);
=> true


// SoftDelete されたデータが検索できないことを確認
//==============================
>>> $Users->get(2);
Cake\Datasource\Exception\RecordNotFoundException with message 'Record not found in table "users"'


// 'withDeleted' をつけることによって SoftDelete されたデータを検索できる
//==============================
>>> $Users->find('all', ['withDeleted'])->where(['id' => 2])->first();
=> App\Model\Entity\User {#521
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#512
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#511
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": Cake\I18n\FrozenTime {#522
       +"time": "2017-11-14T12:42:48+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// restore によって SoftDelete されたデータを元に戻せる
//==============================
>>> $Users->restore($entity);
=> App\Model\Entity\User {#216
     +"created": Cake\I18n\Time {#232
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\Time {#229
       +"time": "2017-11-14T12:44:30+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"id": 2,
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// restore の結果について確認
//==============================
>>> $Users->get(2);
=> App\Model\Entity\User {#548
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#539
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#538
       +"time": "2017-11-14T12:44:30+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// hardDelete をすることによって、復元できない削除をする
//==============================
>>> $Users->hardDelete($entity);
=> true


// hardDelete の結果について確認
//==============================
>>> $Users->get(2);
Cake\Datasource\Exception\RecordNotFoundException with message 'Record not found in table "users"'

>>> $Users->find('all', ['withDeleted'])->where(['id' => 2])->first();
=> null

かゆいところに手を伸ばす

デフォルトでは deleted というカラムだが、テーブルにプロパティを追加することで違うカラム名にもできる。

// UserTable.php
...

use SoftDelete\Model\Table\SoftDeleteTrait;

class UsersTable extends Table
{
    use SoftDeleteTrait;

    protected $softDeleteField = 'kokoga_deleted_no_field_desu';

    ..

とはいえ CakePHP3 におけるデフォルトのタイムスタンプのフィールドは created と modified という名前になっているので、そこは deleted という名前で合わせにいくのがわかりやすさも高くて良いと思う。

php-amqplib と RabbitMQ について調べた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

見慣れないライブラリを使ってるプロジェクトがあったので調査メモ。

php-amqplibとは?

php-amqplib/php-amqplib: AMQP library for PHP

RabbitMQ とよばれるメッセージングキューのためのクライアントライブラリ for PHP 。厳密には RabiitMQ という話ではなく、そこで利用されるAMQP と呼ばれるプロトコルに準拠したものを取り扱う。メッセージのやり取りについてその専用のプロトコルでやりとりしている。
利用するには mbstring と bcmath のエクステンションが必須になる。

php-amqplib は packagist で管理されているので composer からインストールすることになる。

RabbitMQ とは?

RabbitMQ - Messaging that just works

うさぎがかわいい。

AMQP というメッセージプロトコルを語れる、メッセージングキュー用のミドルウェア…、という分類でいいのかな。

Javascript の Promise みたいなイメージでいると多分認識が合う(合わない)
メッセージを送る人と受け取る人、そして運び人のウサギがいる。ウサギが様々なメッセージのやり取りや順序管理をしてくれるので、送る人は誰に届くかわからないけどこの処理できる人やって、受け取る方は誰から来たかわからないけど処理して結果をしまっておく、なんてことが出来る。
(=マイクロサービスなアーキテクチャ)

ちなみに Erlang で書かれている。
ちなみに今年で 10 年経つんだとかでトップページがお祝いムード。

 

手軽に試すなら公式イメージがあるので docker で。

library/rabbitmq - Docker Hub

 

そうでなければ公式サイトを参考にインストールしていくとよい。

RabbitMQ - Downloading and Installing RabbitMQ

CentOS の項目をみるとこんなことが書かれている。

Overview
rabbitmq-server is included in Fedora. However, the versions included are often quite old. You will probably get better results installing the .rpm from PackageCloud or Bintray. Check the Fedora package details for which version of the server is available for which versions of the distribution.

Fedora には rabbitmq-server な名前で既に登録されているものがあるが、古い可能性ので Package Cloud の RPM を使うのがオススメする、って書いてあった。

rabbitmq/rabbitmq-server - Packages - packagecloud.io | packagecloud

 

ちなみに、ちょっと昔の記事なので、バージョンが古くて今と異なる部分があるかもだが、実際に業務運用に向けていくようなところも込で、がっつりとした情報があったので、こちらも合わせて読むと実践向けっぽい。

はじめての RabbitMQ|サイバーエージェント 公式エンジニアブログ

PHPから使ってみる

例によってお手軽に試してみる程度。 docker で準備を進めていく。

// docker でキューサーバを立ち上げてみる
// 5672 ポートをバインドするのを忘れずに
$ sudo docker run -d -p 5672:5672  --name rabbit-server rabbitmq:3
...

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                                                   NAMES
92547dbde579        rabbitmq:3          "docker-entrypoint.s   5 seconds ago       Up 4 seconds        4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, 25672/tcp   rabbit-server


// モジュールが必要なので入れる(mbstringはもともとあった)
$ sudo yum install php-bcmath --enablerepo=remi-php70

// モジュールを移動しないとダメだった(パス設定してないだけだと思う)
$ sudo mv /etc/php.d/20-bcmath.ini /etc/opt/remi/php70/php.d/
$ sudo mv /usr/lib64/php/modules/bcmath.so /opt/remi/php70/root/usr/lib64/php/modules/

$ php -m | grep bcmath
bcmath


// アプリを構築する
$ mkdir php-amqplib-test
$ cd php-amqplib-test

$ composer init
...

$ composer require php-amqplib/php-amqplib:2.7.*
...

ソースを書く。

<?php
// send.php

require('vendor/autoload.php');

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

// デフォルトID/PW = guest/guest
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');

// 送信するチャンネルを設定
$channel = $connection->channel();

// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L597
// string queue = ''
// bool passive = false
// bool durable = false
// bool exclusive = false
// bool auto_delete = true
// bool nowait = false
$channel->queue_declare('hello', false, false, false, false);


// メッセージ作成
$msg = new AMQPMessage('Hello World!');

// メッセージ送信
// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L1086
// string msg
// string exchange = ''
// string routing_key = ''
$channel->basic_publish($msg, '', 'hello');
<?php
// receive.php

require('vendor/autoload.php');

use PhpAmqpLib\Connection\AMQPStreamConnection;

// デフォルトID/PW = guest/guest
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');

// チャンネルを設定
$channel = $connection->channel();

// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L597
// string queue = ''
// bool passive = false
// bool durable = false
// bool exclusive = false
// bool auto_delete = true
// bool nowait = false
$channel->queue_declare('hello', false, false, false, false);


// コールバック関数の用意
$callback = function ($msg) {
        echo $msg->body . "\n";
};

// メッセージ受信
// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L901
// string queue = ''
// string consumer_tag = ''
// bool no_local = false
// bool no_ack = false
// bool exclusive = false
// bool nowait = false
// function callback = null
$channel->basic_consume('hello', '', false, true, false, false, $callback);

// メッセージ受信待ちループ
while(count($channel->callbacks)) {
    $channel->wait();
}

なお引数に関するコメントはデフォルトの値。これが何を意味しているかはまたちょっと分からない…。ドキュメントを読むしか…。

ちなみに PHP の API ドキュメントは見当たらない。 Github のソースを読むか、インタフェースはだいたい同じなので JavaDoc のものを読むかするとよい。

 

ターミナルを2つ開いて実行してみる。

// 一方のターミナル
$ php send.php
$ php send.php
$ php send.php
$ php send.php

// もう一方のターミナル
$ php receive.php
Hello World!
Hello World!
Hello World!
Hello World!

ただし、これには問題があって、このように単純に送るだけだとキューが RabbitMQ を再起動したりマシンが落ちたりすると、中身がきれいさっぱり消える。

$ php send.php
$ sudo docker restart rabbit-server
$ php receive.php

// でてこない!

公式ドキュメントだとこのページの Message durability の項目に書いてあるが、送信時に引数を設定しないといけない。

RabbitMQ - RabbitMQ tutorial - Work Queues

<?php
require('vendor/autoload.php');

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

// デフォルトID/PW = guest/guest
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');

// 送信するチャンネルを設定
$channel = $connection->channel();

// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L597
// string queue = ''
// bool passive = false
// bool durable = false
// bool exclusive = false
// bool auto_delete = true
// bool nowait = false
$channel->queue_declare('hello', false, true, false, false);


// メッセージ作成
// デリバリモードの追加
$msg = new AMQPMessage('Hello World!', ['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]);

// メッセージ送信
// https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Channel/AMQPChannel.php#L1086
// string msg
// string exchange = ''
// string routing_key = ''
$channel->basic_publish($msg, '', 'hello');
// 一旦再起動しておく
$ sudo docker restart rabbit-server

// 送信, 再起動, 受信
$ php send.php
$ sudo docker restart rabbit-server
$ php receive.php
Hello World!

なるほどね~。

ちなみにの話

ちなみに Laravel のキュードライバとして RabbitMQ を使うライブラリも既にあり、これはもう入れるだけでいい。先人の知恵すごい。

vyuldashev/laravel-queue-rabbitmq: RabbitMQ driver for Laravel Queue

書いてある通りです、以上の説明は出来なくて。要するに QUEUE_DRIVER を変更し、 RabbitMQ 用の設定を幾つか書けば勝手にやってくれるのだそうだ。

ちなみに上記の durable 、メッセージの永続化はデフォルトで有効になっているようだ。

 

ちなみにちなみに、メッセージキューの実装を標準化しよう!という話があるっぽい。知らなかった。動きとしては将来的に PSR になったらいいな、くらいの強制していないけどぼちぼち運動してます!なもの。

queue-interop/queue-interop: Promoting the interoperability of MQs objects. Based on Java JMS

書き方、インタフェースが大きく分かれそうだし、複数対応を謳うようなライブラリもあるし、標準化されるといろいろ嬉しい予感がする。

残念ながら ISUCON 7 は予選敗退で幕を閉じた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

ISUCON 5, ISUCON 6, と引き続き ISUCON 7 に参加しました。

ISUCON についてはこのあたりを見ると良いです。
ISUCON7 まとめ : ISUCON公式Blog

今回もぼっちーむを回避して、会社の人と組んでいきました。前回の ISUCON 6 と同じメンバーです。

過去の参戦レポはこのあたり参照するとよいです。

今回は、ぼくの参戦時間が実質 3 時間程度、少ない時間しか当てられませんでした。
(ぼくからみた義親、つまりは妻の両親が家庭訪問という重大なイベントが発生して、椅子コンよりも優先度高めだった)

当日までにやっておいたこと

メンバーで集まって、何度か方針や準備するものについて話あっていました。準備するのは当日にあたふたしないように、ある程度は機械的に出来るように、というところからです。
具体的にはこんなものが挙がりました。

  • さくらクラウドのアカウント準備
    • クーポン投入
    • お金をケチらない気持ち。必要なら大規模マシンで殴って戻すことも視野に。
  • 個々人の環境
    • ISUCONイメージがさくらクラウドから提供される?情報まち。
    • されないならubuntu使って空っぽマシン用意
      • 中身をがばっともってくる
  • 簡単に色々なログ仕込める君
    • スクリプトとかansible的なやつでやりたいよね
    • mysql-slowとかアクセスログとか。
    • CPUとかメモリ、ネットワークの様子もみたいよね
    • プロファイリングツールの準備
      • alp
      • new relicをいれたい
        • 準備だけしておきたい
        • PHPの計測もできるが細かに計測する記述をしないといけない
      • Kibana
        • mysql も
        • td-agent でいろいろ飛ばせば全部見れる
  • ssh公開鍵の準備
    • 鍵をcurlするだけでつなげるような状態にしたい
  • gitリポジトリ作る
    • 鍵とssh-configとを準備する
  • つけるべきHTTPヘッダの整理
    • 304 not modified, expire + etag
    • 200 ok
    • 301 / 302 転送
  • 画像圧縮
    • ツールや構築の下準備
  • ご飯@ランチの用意
    • 当日朝各自で買ってこよう
    • 大人なのでお菓子とアルコールはご自由に!
  • FWになれる
  • 実装の読み解き
    • 頭の中でやるとつらい
    • ホワイトボードに書こう
    • 付箋を使おう
  • 当日の流れを作っておく

ISUCON のたびに毎回考えるのはアレなので、そろそろメンテしながら使いまわしをしていきたい所存。

それと、練習しよっ♡、なんて言ってたけど結局一回もやってない。いや、環境構築までは一回やった。 vagrant-isucon という素晴らしいものがあって、これを使わせてもらった。当時行われた時の得点まで伸びはしないだろうが、何をしたらどう伸びるのか、という部分は見ていける、便利ちゃん。

matsuu/vagrant-isucon: ISUCON過去問を構築するためのVagrantfile集

ちなみに結局当日になっても、このリストの大半は実施あるいは準備されませんでした。。熱量の違いだったり、そういうアレ。

当日にやったこと

合流したのは18時過ぎ。それまでに以下のことをチームメンバーが進めていました。

  • phpへの切り替え
  • リポジトリの準備
  • /messages/ の N+1 の解消
  • インデックス設定
  • 画像ファイルを DB から取り出し DB に保存しないようにする
  • nginx, mysql, php-fpm の微調整

合流したときにはスコア的には 1 万くらい?
( っ˘ω˘c).。o○( なんでスコア伸びてないんだ…? )

そこからはこんなことを進めていきました。

  • 画像圧縮するしないで揉めてたっぽいのでやるなって言った
  • それするより画像に 304 つけろって言った
  • LINE 通話をつなげた
  • ハッピーターンでお腹を満たす
  • /fetch の N+1 改善
  • nginx の設定サポート
  • /message や /login あたりの微妙な改善

時間も少なく、現場まで合流するのは厳しかったのでリモートで作業せざるを得ませんでした。チャットだけでも良かったんですが、声が繋がったほうが何かと便利なので LINE 通話を PC で繋ぎっぱなしにして、あーだこーだ言いながらやってました。

最終スコアは 37000 くらい、だったかな?(結果一覧に出ている点数と微妙に違うっぽい?)

ISUCON 5 とくらべて、アプリ上の実装の問題はほとんど消化できたと思っていて、そのあたりは前回からレベルアップしたと思う(チームとして)

画像の304については理解が浅くて結局出来ていなかった。周りの攻略記事を見ると、ここが出来る出来ないでスコアが大きく変わっていたように思える。最後までベンチマーク実行結果に icons のレスポンスが遅くて~、とあって、これが改善できないともうスコア伸びないってのは分かっていたけど、いろいろなものを信用しすぎて何も動けなかった。

結局よくわかんなくて合流してからずっと右往左往したところがあって、ベンチマーク時に 2 つチェックいれたらどうなんねん、というところ。リバースプロキシしてバランシングしたり、外したり、そもそも設定うまく行かなくて、だいぶ時間がかかった。結局 2 つチェックいれたらベンチが倍速になってスコア上がるんかな?どこかに説明あったっけ(見落としたかも?)

サーバ構成的には AP + AP + DB という、多分他と同じ構成。

次に向けて

ISUCON が来年も実施されるかわからないけれども(ものすごく楽しいイベントなので実施されてほしい)、自分のスキルアップはもちのろん、チーム側の意識・熱量・レベル感が合ってないどうすっかなあ、というところをやっていかないと勝てないだろうなって気持ち。例えばあった話だと、とりあえず画像圧縮してプロキシキャッシュすれば勝てるやろ~~、とか真面目に雑な話をしている人とか。19時くらいになってわかんないからスロークエリ入れるか~~~とか言ってたり、先やれよ。とか。うごかないんだけど見てくれ~って、別にこっちも分からないから都度調べてるんだけどなあ…。とかなんとか、こんな人と仕事していたのかとか云々。余談が過ぎた。ようは 3 人で役割を分けてそれぞれ最高のパフォーマンスをしていくべきで、それは 2 人になっても変わらなくて、ぼくが居ないまたは手が出せない状況だろうと頑張ってくれって気持ち。なのでそれが足りないっぽいのでチームビルディングというか、そういう類の云々がだめだめだったんだろうなあ~~。

もし次があったとして、次も同じチームででるかっていうと怪しく、楽しく全力で戦えるチームで出来るといいな。どんどんやっていこう。
結果として ISUCON 7 は予選敗退だったけれども、俺の、俺たちの、 ISUCON 7 はまだ終わってない!(練習しよ

そんなポエムで〆


ISUCON 運営の人たちメチャ忙しいと思いますけど、こうやって今年も楽しいイベントを開催してくれてありがとうございました?

前後の記事

Next:
Prev:

composer create-project は何が起こるのか

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

CakePHP 3 のクイックスタートを改めて見てたら composer create-project ... ってでてきて何だこいつは…ってなった話。

Quick Start Guide - 3.x

php composer.phar create-project --prefer-dist cakephp/app bookmarker

composer create-project がやること

要はドキュメントにあるので読めって話になる。

Command-line interface / Commands - Composer

You can use Composer to create new projects from an existing package. This is the equivalent of doing a git clone/svn checkout followed by a "composer install" of the vendors.

There are several applications for this:

1. You can deploy application packages.
2. You can check out any package and start developing on patches for example.
3. Projects with multiple developers can use this feature to bootstrap the initial application for development.

To create a new project using Composer you can use the "create-project" command. Pass it a package name, and the directory to create the project in. You can also provide a version as third argument, otherwise the latest version is used.

ざっくりと翻訳してみると、こんなことが書かれているようだ。

既にあるパッケージから新しいプロジェクトを作ります。それは git clone または svn checkout をして composer install を行うのと同等です。

1. アプリケーションパッケージをデプロイできる
2. パッケージをチェックアウトして開発をしていける
3. 新しい開発のため、ブートストラップな初期化を提供ができる

"create-project" コマンドによってそれは実行できる。パッケージ名、プロジェクトのディレクトリ名、そしてパッケージバージョンを指定できる。

CakePHP のドキュメントにあったコマンドを読み解く

php composer.phar create-project --prefer-dist cakephp/app bookmarker

まず指定されているパッケージを見てみる。

cakephp/app - Packagist

A skeleton for creating applications with CakePHP 3.x.

なるほど。つまり、空っぽの CakePHP アプリケーションがパッケージ登録されているので、それをもってくることで新規に空っぽの CakePHP アプリケーション作れるやん!ということだそうだ。

ところで --prefer-dist はなんだろう。 composer のドキュメントをもう一度見る。

--prefer-dist: Install packages from dist when available.

んーむ。 dist があればインストールするよって言ってるけどなんのことやら…? → install の項目にガッツリと書いてある。

--prefer-dist: Reverse of --prefer-source, Composer will install from dist if possible. This can speed up installs substantially on build servers and other use cases where you typically do not run updates of the vendors. It is also a way to circumvent problems with git if you do not have a proper setup.

--prefer-source の逆で、 dist からのインストールを優先する。これによってビルドサーバやアップデートをしないような環境では高速に動作する。あるいは git の設定をしてないときに問題を解決出来る。

--prefer-source の説明も合わせて読んだところ、どうやら composer は dist って呼ばれるところを基本的には利用するらしい。 source はバージョン管理リポジトリで、直接 git clone するようなものになっているので、設定をしてないと問題が起きたりするそう。 Github を直接見に行ってエラー、とかそういう感じなのかな?まあ、デフォルトでは dist なので --prefer-dist はあまり気にしなくても良さそう。

で。

3 つめの引数に書いてある bookmarker が作られるディレクトリだ。
カレントディレクトリに bookmarker というディレクトリが作られ、その中に cakephp/app パッケージの内容が展開されているはず。加えて composer install も行われているので bookmarker/vendor ディレクトリも作られている。

まとめ

要は git clone しまっせ、みたいなものだと思っておけばだいたい良さそうだ。

NGINX Unit なるものリリースされたらしいのでとりあえず PHP でも動かしてみる

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

NGINX Unit なるものがリリースされたらしい。

・NGINX 公式サイトの情報
NGINX Unit

・NGINX Unit 公式サイト
NGINX Unit

・見かけた記事
NGINXからアプリケーションサーバ「NGINX Unit」がオープンソースで登場。PHP、Go、Pythonに対応。Java、Node.jsにも対応予定 - Publickey

ドキュメントに書かれているインストール方法や設定を見てたらパッと出来そうだったので思いたってやったったの巻。

どうにも公式サイトのドキュメンテーションを見ると CentOS と Ubuntu 向けにはパッケージが適当されているっぽい。なのでコレを利用する。また nginx が既にいる環境でやると便利そうなので docker で立ち上げていく

// ホストにて
$ docker run --name nginx-unit -d -p 8080:80 nginx
$ docker exec -it nginx-unit bash

// 以降はコンテナ内、ゲストで作業

// 準備
# apt-get update
# apt-get install curl net-tools vim less

// キーの登録
# curl http://nginx.org/keys/nginx_signing.key > /tmp/nginx_signing.key
# apt-key add /tmp/nginx_signing.key

// /etc/apt/sources.list に追記
# vim /etc/apt/sources.list
deb http://nginx.org/packages/mainline/ubuntu/ xenial nginx
deb-src http://nginx.org/packages/mainline/ubuntu/ xenial nginx

// nginx-unit のインストール
# apt-get update
# apt-get install unit


// コンフィグ確認
# cat /etc/init.d/unit
CONFIG=/etc/unit/config

// /var/run/control.unit.sock が設定用のソケットファイルっぽい
    dumpconfig)
        curl -sS --unix-socket /var/run/control.unit.sock localhost >${CONFIG}.new



# vim /etc/unit/config
{
     "listeners": {
         "*:8300": {
             "application": "myapp"
         }
     },
     "applications": {
         "myapp": {
              "type": "php",
              "workers": 3,
              "root": "/var/www/html/",
              "index": "index.php"
         }
     }
}

// 起動
# service unitd start

// コンフィグの確認
# curl --unix-socket /var/run/control.unit.sock http://localhost/
{
        "listeners": {},
        "applications": {}
}

// コンフィグの変更してみる
# service unitd stop
# mv /etc/unit/config /etc/unit/myconfig.json
# service unitd start

# curl -X PUT -d @/etc/unit/myconfig.json --unix-socket /var/run/control.unit.sock http://localhost/
curl: (52) Empty reply from server

# curl --unix-socket /var/run/control.unit.sock http://localhost/

// レスポンスが返ってこない

// ログ確認
# less /var/log/unitd.log

// ... とくにエラーっぽいものなし。

パッケージのものだとうまく動かない???

ということでソースから入れる路線を試す。

// パッケージからのものを消す
# apt-get remove unit

// 準備
# apt-get install git build-essential php php-dev libphp-embed
# cd /tmp
# git clone https://github.com/nginx/unit
# cd unit

// ビルド
# ./configure --prefix=/usr/share/unit/
# ./configure php
# make all

// インストール
# make install
# ln -s /usr/share/unit/sbin/unitd /sbin/unitd

// 起動
# unitd

// 設定してみる
# vim /tmp/unit_config.json

{
     "listeners": {
         "*:8300": {
             "application": "myapp"
         }
     },
     "applications": {
         "myapp": {
              "type": "php",
              "workers": 3,
              "root": "/var/www/html/",
              "index": "index.php"
         }
     }
}


# curl -X PUT -d @/tmp/unit_config.json --unix-socket /usr/share/unit/control.unit.sock http://localhost/
{
        "success": "Reconfiguration done."
}

# curl --unix-socket /usr/share/unit/control.unit.sock http://localhost/
{
        "listeners": {
                "*:8300": {
                        "application": "myapp"
                }
        },

        "applications": {
                "myapp": {
                        "type": "php",
                        "workers": 3,
                        "root": "/var/www/html/",
                        "index": "index.php"
                }
        }
}

// 動作確認
# echo "<?php echo 'hello php';" > /var/www/html/index.php
# curl localhost:8300
hello php

// nginx からプロキシしてみる
# vim /etc/nginx/conf.d/default.conf

upstream unit_backend {
    server 127.0.0.1:8300;
}

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /var/www/html;
        index  index.html index.htm;
    }

    location ~ \.php$ {
        proxy_pass http://unit_backend;
        proxy_set_header Host $host;
    }
}

// 動作確認
# service nginx reload
Reloading nginx: nginx.

# curl localhost/index.php
hello php

出来ているようなので、もう少しちゃんと PHP を動かしてみる。

// phpモジュール追加
# apt-get install php-mbstring php-zip

// composer 導入
# curl https://getcomposer.org/installer > installer
# php installer
# rm installer
# chmod +x composer.phar

// Laravelプロジェクトインストール
# ./composer.phar create-project laravel/laravel app

// Laravel プロジェクトの色々設定
# chmod -R 777 /var/www/html/app/storage/
# php artisan key:generate

// nginx-uniti のルートディレクトリを変更
#  curl -X PUT -d '"/var/www/html/app/public/"' --unix-socket /usr/share/unit/control.unit.sock http://localhost/applications/myapp/root
{
        "success": "Reconfiguration done."
}

// 確認
# curl --unix-socket /usr/share/unit/control.unit.sock http://localhost/
{
        "listeners": {
                "*:8300": {
                        "application": "myapp"
                }
        },

        "applications": {
                "myapp": {
                        "type": "php",
                        "workers": 3,
                        "root": "/var/www/html/app/public/",
                        "index": "index.php"
                }
        }
}


// nginx のルートディレクトリを変更
# vim /etc/nginx/conf.d/defualt.conf

    location ~ / {
        proxy_pass http://unit_backend;
        proxy_set_header Host $host;
    }

# service nginx reload


// 動作確認
# curl localhost

// 何か出ていそうなのでブラウザで開く…!

イエーイ! NGINX Unit で Laravel アプリケーションが動いたぞ~~~空っぽだけど…。

ご覧とおり php-fpm は動いてなくて unit のワーカーが動いてるだけですねー、しゅごい。

# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  32648  3464 ?        Ss   00:50   0:00 nginx: master process nginx -g daemon off;
root         8  0.0  0.0  18224  2176 ?        Ss+  00:51   0:00 bash
root     26019  0.0  0.0  22196   852 ?        Ss   03:00   0:00 unit: main [unitd]
nobody   26021  0.0  0.0  32440   736 ?        S    03:00   0:00 unit: controller
nobody   26022  0.0  0.0 204620  1440 ?        Sl   03:00   0:00 unit: router
nobody   27293  0.0  0.7 269004 28232 ?        S    03:28   0:00 unit: "myapp" application
nginx    27330  0.0  0.0  33088  2152 ?        S    03:31   0:00 nginx: worker process
root     27331  1.2  0.0  18140  2064 ?        Ss   03:41   0:00 bash
root     27337  0.0  0.0  36572  1588 ?        R+   03:41   0:00 ps aux

現状では Go / PHP / Python に対応しているようで言語環境の準備と configure 時に言語指定すれば使えるようです。

手元にいい感じにお試せるアプリケーションがいないので細かい挙動の様子をみるなど出来ていないのですが、リリースニュース直後からはてぶが盛り上がったり github も盛り上がりを見せている(ように見える)ので、ちょっとしたバグなどなどあってももりもり直されていくんじゃないかなーという予感です。

そのうち Apache + mod_php / nginx + php-fpm / nginx + unit などでパフォーマンスも比べていきたいですねー!

前後の記事

Next:
Prev:

Laravel でマイグレーションなどの artisan コマンドを実行するときのエラーを詳し表示した

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

artisan コマンドでエラーが出た場合、そのまま実行していると例外クラスとメッセージが出る程度で、どこでエラーになっとんねん!がわかりません。

実は自分で作った artisan コマンド含めてすべての artisan コマンドは verbose なオプションをサポートしています。そのためオプションを指定するだけで、例外を詳しく見ることができます。

$ php artisan migrate

  [Illuminate\Database\QueryException]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists (SQL: alter table `my_tables` drop `created_at`, drop `updated_at`)

  [Doctrine\DBAL\Driver\PDOException]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists
  
  [PDOException]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists 


$ php artisan migrate -v
                                                                                       
  [Illuminate\Database\QueryException (42000)]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists (SQL: alter table `my_tables` drop `created_at`, drop `updated_at`)

Exception trace:
 () at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:647
 Illuminate\Database\Connection->runQueryCallback() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:607
 Illuminate\Database\Connection->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:450
 Illuminate\Database\Connection->statement() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php:86
 Illuminate\Database\Schema\Blueprint->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:239
 Illuminate\Database\Schema\Builder->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:148
 Illuminate\Database\Schema\Builder->table() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:221
 Illuminate\Support\Facades\Facade::__callStatic() at /var/www/myapp/database/migrations/2017_08_28_190905_modify_my_tables.php:21
 ModifyMstGraphTransitionTerms11->up() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:357
 Illuminate\Database\Migrations\Migrator->Illuminate\Database\Migrations\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:363
 Illuminate\Database\Migrations\Migrator->runMigration() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:177
 Illuminate\Database\Migrations\Migrator->runUp() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:146
 Illuminate\Database\Migrations\Migrator->runPending() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:95
 Illuminate\Database\Migrations\Migrator->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/MigrateCommand.php:69
 Illuminate\Database\Console\Migrations\MigrateCommand->fire() at n/a:n/a
 call_user_func_array() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:29
 Illuminate\Container\BoundMethod::Illuminate\Container\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:87
 Illuminate\Container\BoundMethod::callBoundMethod() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:31
 Illuminate\Container\BoundMethod::call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/Container.php:539
 Illuminate\Container\Container->call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:182
 Illuminate\Console\Command->execute() at /var/www/myapp/vendor/symfony/console/Command/Command.php:264
 Symfony\Component\Console\Command\Command->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:167
 Illuminate\Console\Command->run() at /var/www/myapp/vendor/symfony/console/Application.php:874
 Symfony\Component\Console\Application->doRunCommand() at /var/www/myapp/vendor/symfony/console/Application.php:228
 Symfony\Component\Console\Application->doRun() at /var/www/myapp/vendor/symfony/console/Application.php:130
 Symfony\Component\Console\Application->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:122
 Illuminate\Foundation\Console\Kernel->handle() at /var/www/myapp/artisan:35


  [Doctrine\DBAL\Driver\PDOException (42000)]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists

Exception trace:
 () at /var/www/myapp/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:107
 Doctrine\DBAL\Driver\PDOStatement->execute() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:449
 Illuminate\Database\Connection->Illuminate\Database\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:640
 Illuminate\Database\Connection->runQueryCallback() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:607
 Illuminate\Database\Connection->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:450
 Illuminate\Database\Connection->statement() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php:86
 Illuminate\Database\Schema\Blueprint->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:239
 Illuminate\Database\Schema\Builder->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:148
 Illuminate\Database\Schema\Builder->table() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:221
 Illuminate\Support\Facades\Facade::__callStatic() at /var/www/myapp/database/migrations/2017_08_28_190905_modify_my_tables.php:21
 ModifyMstGraphTransitionTerms11->up() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:357
 Illuminate\Database\Migrations\Migrator->Illuminate\Database\Migrations\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:363
 Illuminate\Database\Migrations\Migrator->runMigration() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:177
 Illuminate\Database\Migrations\Migrator->runUp() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:146
 Illuminate\Database\Migrations\Migrator->runPending() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:95
 Illuminate\Database\Migrations\Migrator->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/MigrateCommand.php:69
 Illuminate\Database\Console\Migrations\MigrateCommand->fire() at n/a:n/a
 call_user_func_array() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:29
 Illuminate\Container\BoundMethod::Illuminate\Container\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:87
 Illuminate\Container\BoundMethod::callBoundMethod() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:31
 Illuminate\Container\BoundMethod::call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/Container.php:539
 Illuminate\Container\Container->call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:182
 Illuminate\Console\Command->execute() at /var/www/myapp/vendor/symfony/console/Command/Command.php:264
 Symfony\Component\Console\Command\Command->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:167
 Illuminate\Console\Command->run() at /var/www/myapp/vendor/symfony/console/Application.php:874
 Symfony\Component\Console\Application->doRunCommand() at /var/www/myapp/vendor/symfony/console/Application.php:228
 Symfony\Component\Console\Application->doRun() at /var/www/myapp/vendor/symfony/console/Application.php:130
 Symfony\Component\Console\Application->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:122
 Illuminate\Foundation\Console\Kernel->handle() at /var/www/myapp/artisan:35


  [PDOException (42000)]
  SQLSTATE[42000]: Syntax error or access violation: 1091 Can't DROP 'created_at'; check that column/key exists

Exception trace:
 () at /var/www/myapp/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:105
 PDOStatement->execute() at /var/www/myapp/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:105
 Doctrine\DBAL\Driver\PDOStatement->execute() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:449
 Illuminate\Database\Connection->Illuminate\Database\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:640
 Illuminate\Database\Connection->runQueryCallback() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:607
 Illuminate\Database\Connection->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Connection.php:450
 Illuminate\Database\Connection->statement() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php:86
 Illuminate\Database\Schema\Blueprint->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:239
 Illuminate\Database\Schema\Builder->build() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php:148
 Illuminate\Database\Schema\Builder->table() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:221
 Illuminate\Support\Facades\Facade::__callStatic() at /var/www/myapp/database/migrations/2017_08_28_190905_modify_my_tables.php:21
 ModifyMstGraphTransitionTerms11->up() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:357
 Illuminate\Database\Migrations\Migrator->Illuminate\Database\Migrations\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:363
 Illuminate\Database\Migrations\Migrator->runMigration() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:177
 Illuminate\Database\Migrations\Migrator->runUp() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:146
 Illuminate\Database\Migrations\Migrator->runPending() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Migrations/Migrator.php:95
 Illuminate\Database\Migrations\Migrator->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Database/Console/Migrations/MigrateCommand.php:69
 Illuminate\Database\Console\Migrations\MigrateCommand->fire() at n/a:n/a
 call_user_func_array() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:29
 Illuminate\Container\BoundMethod::Illuminate\Container\{closure}() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:87
 Illuminate\Container\BoundMethod::callBoundMethod() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php:31
 Illuminate\Container\BoundMethod::call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Container/Container.php:539
 Illuminate\Container\Container->call() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:182
 Illuminate\Console\Command->execute() at /var/www/myapp/vendor/symfony/console/Command/Command.php:264
 Symfony\Component\Console\Command\Command->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Console/Command.php:167
 Illuminate\Console\Command->run() at /var/www/myapp/vendor/symfony/console/Application.php:874
 Symfony\Component\Console\Application->doRunCommand() at /var/www/myapp/vendor/symfony/console/Application.php:228
 Symfony\Component\Console\Application->doRun() at /var/www/myapp/vendor/symfony/console/Application.php:130
 Symfony\Component\Console\Application->run() at /var/www/myapp/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:122
 Illuminate\Foundation\Console\Kernel->handle() at /var/www/myapp/artisan:35

ちなみにサポートするオプションと、表示レベルは以下のようになっています。


// /vendor/laravel/framework/src/Illuminate/Console/Command.php
// ※抜粋

use Symfony\Component\Console\Output\OutputInterface;

protected $verbosity = OutputInterface::VERBOSITY_NORMAL;

protected $verbosityMap = [
    'v' => OutputInterface::VERBOSITY_VERBOSE,
    'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE,
    'vvv' => OutputInterface::VERBOSITY_DEBUG,
    'quiet' => OutputInterface::VERBOSITY_QUIET,
    'normal' => OutputInterface::VERBOSITY_NORMAL,
];
オプション レベル 生数値
v OutputInterface::VERBOSITY_VERBOSE 64
vv OutputInterface::VERBOSITY_VERY_VERBOSE 128
vvv OutputInterface::VERBOSITY_DEBUG 256
quiet OutputInterface::VERBOSITY_QUIET 16
normal OutputInterface::VERBOSITY_NORMAL 32

まあ、これってソースのとこにもあるんですけど、ベースになってる Symfony2 のものなんですけどね。

Verbosity Levels (current)

Symfony2 で定義されている値を使って artisan コマンドも同じように verbose な指定をサポートしているんですねー。もし自分で作るコマンドも verbose な指定をサポートするなら、指定されている定数を目安に出力すると良さそう。

1 2 3 4