技術的な話題 に関する投稿を表示しています

docker で MySQL を立ち上げたい。

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

タイトルそのまま。

// たぶん mysql 5.7 がプルされる
// が、わからんので DockerHub を要確認
$ docker pull mysql:latest

// mysqlコンテナの起動
//   オプションについて
//     -e    : 環境変数
//     --name: コンテナ名
//     -d    : バックグラウンド
//     -p    : ポートバインディング(ホスト:ゲスト)
//   環境変数について
//     MYSQL_DATABASE     : 作るデータべース
//     MYSQL_USER         : 作るユーザ
//     MYSQL_PASSWORD     : 作るユーザのパスワード
//     MYSQL_ROOT_PASSWORD: ルートパスワード
$ docker run --name my_mysql \
  -e MYSQL_DATABASE=homestead \
  -e MYSQL_USER=homestead \
  -e MYSQL_PASSWORD=secret \
  -e MYSQL_ROOT_PASSWORD=secret \
  -d \
  -p 13306:3306 \
  mysql

// 接続確認
//   -h : ホスト名
//   -P : ポート番号
//   -u : ユーザ名
//   -p : パスワードを入力するプロンプトを出す
$ mysql -h 127.0.0.1 -P 13306 -u root -p

記事書くときに Docker Hub を確認したら 5.7.19 が latest で指定されているっぽい。というか 8.0 系も指定できるのか、検証なんかにも便利そうだ。

library/mysql - Docker Hub

でもって docker で MySQL が立ち上がると、こうさ、ほら、ユニットテスト回すときなんかに、こうしてクリーンな DB を提供できてよさそうだなーって。
イメージとしてはこんな感じ。

{
    ...
    "scripts": {
        "test": "phpunit --verbose",
        "docker-test": [
            "composer docker-db-remove"
            "composer docker-db-create",
            "composer test",
            "composer docker-db-remove"
        ],
        "docker-db-create": "docker run --name test_mysql -e MYSQL_DATABASE=homestead -e MYSQL_USER=homestead -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=secret -d -p 13306:3306 mysql",
        "docker-db-remove": "docker rm --force test_mysql",
        "docker-db-connect": [
          "#  Copy this command!",
          "#  mysql --host=127.0.0.1 --port=13306 --user=homestead --password=secret homestead"
        ]
    },
    ...
}

composer docker-test とやったらコンテナが立ち上がり 127.0.0.1:13306 で待ち受けているので、そこを指定するように phpunit.xml だとか .env だとかで DB 接続用の値を設定すればよい。テストの後はコンテナがポイされるので、テスト用のDBはいつでもクリーンに。テストでコケても大丈夫なように、最初にコンテナをポイしたほうがいいのかな。

composer スクリプトで I/O を全部引き渡す方法がわからなかったので docker-db-connect タスクが非常にあやしい感じだけど、これくらいでもまあまあ運用していけるんじゃないかなーー

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 な指定をサポートするなら、指定されている定数を目安に出力すると良さそう。

Google Analytics API を使ってアシストコンバージョンを取得したかった

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

Google Analytics の API を使ってアシストコンバージョンをとりたいが、通常のセッション数などを取得できる Core Reporting API にはアシストコンバージョンという指標は無く取得することができない。 Multi-Channel Funnels Reporting API を使うことでその指標は取得できるようになる。がトラップがあり、やりたいことはできなかったので、タイトルが過去形。

なお MCF API のドキュメントにあるサンプルでは Java で書かれているが、言語別のライブラリが持っているインターフェースはおよそ同じなので、読み換えればよい。

Multi-Channel Funnels Reporting API - デベロッパー ガイド  |  アナリティクス Multi-Channel Funnels Reporting API  |  Google Developers

PHP ならこんな具合にして MCF をコールできる。

// セッション準備
session_start();

// API利用のための認証
//==================================================
$client = new \Google_Client();

// クライアントキー、シークレットの設定
// Developer Console で登録すると発行される
$client->setClientId('...');
$client->setClientSecret('...');

// オフラインアクセスを付与するとリフレッシュトークンを使って無期限にアクセスできる
$client->setAccessType('offline');

// 認証画面を都度表示する
$client->setApprovalPrompt('force');

// リダイレクトURLの設定
// Developer Console で設定した値と合わせないとエラーになる
$client->setRedirectUri('.....');

// アクセス権限の設定
$client->addScope([\Google_Service_Analytics::ANALYTICS_READONLY]);


// 認証が通ってくるとcodeがついてくるのでコレを利用して認証を通す
if (!empty($_GET['code'])) {
    unset($_SESSION['refresh_token']);
    $client->fetchAccessTokenWithAuthCode($_GET['code']);
    $_SESSION['refresh_token'] = $client->getRefreshToken();

} else {

    // リフレッシュトークンがなければ認証URLに飛ばす
    // とりあえず試すならセッションから読み込むと良い。
    if (empty($_SESSION['refresh_token'])) {
        header('Location: ' . $client->createAuthUrl());
        exit;
    }

    // リフレッシュトークンで認証する
    $client->refreshToken($_SESSION['refresh_token']);
}


// Google Analytics とのやり取りするオブジェクト準備
//==================================================
$analytics = new \Google_Service_Analytics($client);


// MCF API を実行する
//==================================================
$gaRequestResult = $analytics->data_mcf->get(
    // プロファイルID
    'ga:......',

    // 開始日
    '30daysAgo',
    // 終了日
    'yesterday',

    // 取得するメトリクス
    'mcf:assistedConversions',

    // 追加のオプション
    [
        'dimensions' => 'mcf:source',
        'filters' => 'mcf:source=@.',
    ]
);


// 結果確認
//==================================================
var_dump($gaRequestResult);

この例だと、流入元別のアシストコンバージョンを30日前から昨日まで範囲で取得する。また流入元の値に "." が含まれていることを条件とする。 流入元とは言うが (direct) のような値も入ってきて厄介。そこで「"." が含まれていること」にすればドメインが付いているものだけ抜けるという仕組み。

Google Developer Console で、このあたりから API 登録をすると動かせるようになる。

https://console.cloud.google.com/apis/dashboard

しかし、実はこの MCF API にはトラップがある。それはディメンションの設定にある。

Google Analytics の画面上からはデバイス別のアシストコンバージョンを確認することができるが API からはこれを確認するすべはない。ドキュメントを見ても MCF API のディメンションにはデバイスの話はない。

ディメンションと指標のリファレンス  |  アナリティクス Multi-Channel Funnels Reporting API  |  Google Developers

また MCF API を呼ぶときにディメンションに Core Reporting API の値を設定してもエラーになってしまう。逆に Core Reporting API を呼び出すときに MCF API のもの(アシストコンバージョンなど)を設定してもエラーになる。

ちなみに Core Reporting API で使える指標はここ。

Dimensions & Metrics Explorer  |  アナリティクス Reporting API v4  |  Google Developers

まあ。アシストコンバージョンそんな重要じゃねーからよくね、って話なのかもしれない。

Chrome の console.log で CSS プロパティが指定できるよ

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

以前からごみばこいんでは、コンソールにようこそメッセージを出していました。

Chrome Devtools のドキュメントを見ると console.log の詳細が書かれています。

診断とコンソールへのログ出力  |  Web  |  Google Developers

ドキュメントには CSS プロパティが利用できるとあるのですが、なぜかこれまでは色とサイズが変えられる程度だと思っていました。試すと text-shadow が出来たり border が出来たりと、様々なプロパティが効くようです。

var properties1 = [
    'font-size: 8em;',
    'color: white;',
    'padding: 5px;',
    'background: linear-gradient(red, orange, yellow, green, aqua, blue, violet);',
    'text-shadow: white 0 0 15px;'
];

var properties2 = [
    'font-size: 8em;',
    'color: #222;',
    'text-shadow: white 0 0 3px;',
    'margin-left: calc(-15em - 6px);',
];
console.log(
    '%cうぇるかむとぅーごみばこいん!%cうぇるかむとぅーごみばこいん!',
    properties1.join(''),
    properties2.join('')
);

いくつかプロパティを試してみたのですが display だったり float だったり position 、 top や left あたりは無効化されているようです。。

で。実は Inspector を使うと、ここも HTML/JS/CSS で構成されていることがわかります。
Inspector は URL バーに chrome://inspect/ と入力するとアクセス出来ます。その中でも Other の項目を見ると Developer Tools について Inspect することができます。

DevTools の DevTools ができるなんてちょっと不思議な光景ですねw

こうやって console.log した結果を眺めてみると %c で指定したものは span タグの style 属性に入っています。
これでは :beforeなどのセレクタが使えなかったり、階層構造が持てないのでアニメーションしたりなどなども出来ないです。先にあった使えないプロパティのところもあるので、凝ったことをするのはできなさそうですねー。残念。

MySQL の文字コードについてチョット調べた

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

MySQL 使ってて utf8_unicode_ci や utf8_general_ci とか mb4 …? とプチ事故が起きたので解消するべく調べた。

照合順序とは

「utf8_general_ci」といった文字コードの指定してるっぽいやつは、文字列カラムにおけるコレーション(=照合順序)と呼ばれる。ここで指定された値によって、MySQL が文字列を解釈したり比較を行うようになっている。

表記は 文字コード_文字セット名_比較方法(※特殊なものとして *_bin もある)

  • 文字コード
    • utf8mb4 とか utf8 とか cp932 とか
  • 言文字セット名
    • japanese とか german とか
    • Unicodeに限り、japanese はないので general か unicode か bin を選ぶ。
      • (言語によって文字の特殊性があるらしく、その言語別の定義があるが日本語はない)
  • 比較方法
    • 3種類あある
    • ci
      • 大文字小文字を比較しない
    • cs
      • 大文字小文字を比較する
    • bin
      • バイナリとして判断する
    • よく使う utf8... では ci と bin のみになる…?(何でー?)

どんな照合順序が使えるかを確認する

show collation というクエリを実行することで確認できる。

show collation;
Collation Charset Id Default Compiled Sortlen
big5_chinese_ci big5 1 Yes Yes 1
big5_bin big5 84 Yes 1
dec8_swedish_ci 8-Dec 3 Yes Yes 1
dec8_bin 8-Dec 69 Yes 1
cp850_general_ci cp850 4 Yes Yes 1
cp850_bin cp850 80 Yes 1
hp8_english_ci hp8 6 Yes Yes 1
hp8_bin hp8 72 Yes 1
koi8r_general_ci koi8r 7 Yes Yes 1
koi8r_bin koi8r 74 Yes 1
latin1_german1_ci latin1 5 Yes 1
latin1_swedish_ci latin1 8 Yes Yes 1
latin1_danish_ci latin1 15 Yes 1
...

※ like など SQL を書くときに使うような演算子を利用して絞り込みができたり、 ORDER BY 並び順を設定できる。

show collation where Collation like 'utf8%';
Collation Charset Id Default Compiled Sortlen
utf8_general_ci utf8 33 Yes Yes 1
utf8_bin utf8 83 Yes 1
utf8_unicode_ci utf8 192 Yes 8
utf8_icelandic_ci utf8 193 Yes 8

MySQL サーバーのバージョンによって使える照合順序が違うので、環境によって結果が変わることがある。(古いと utf8mb4 がないとかそういう) 5.6, 5.7 あたりならこの記事の話は問題ないはず。詳しい一覧はここで確認ができる。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 10.1.14 MySQL でサポートされる文字セットと照合順序

*_unicode_ci と *_general_ci の違い

照合順序の違いによってどんなことが起こるか、実際にクエリを発行しつつ確認する。

# バージョン確認
select version();
# 5.7.17


# アルファベットの大文字小文字、半角全角
#============================================================
SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';
select 'A' = 'a', 'A' = 'A', 'A' = 'a';
# 1, 1, 1
 
SET NAMES 'utf8' COLLATE 'utf8_general_ci';
select 'A' = 'a', 'A' = 'A', 'A' = 'a';
# 1, 0, 0
 
SET NAMES 'utf8' COLLATE 'utf8_bin';
select 'A' = 'a', 'A' = 'A', 'A' = 'a';
# 0, 0, 0
 
 
# ひらがな, 半角カタカナ + 半濁音
#============================================================
SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';
select 'は' = 'パ';
# 1
 
SET NAMES 'utf8' COLLATE 'utf8_general_ci';
select 'は' = 'パ';
# 0
 
SET NAMES 'utf8' COLLATE 'utf8_bin';
select 'は' = 'パ';
# 0

この結果をまとめると。

半角全角の比較 大文字小文字の比較 ひらがなカタカナの比較
unicode_ci できない できない できない
general_ci できる できない できる
bin できる できる できる

というわけで general_ci か bin を使うようにするとこのあたりの比較がちゃんと出来てよさそう。

utf8 と utf8mb4 の違い

MySQL の UTF-8 は 3 バイトの幅しかなく、絵文字などを表現するためには 4 バイトが必要。きっと歴史的な背景から utf8 と utf8mb4 が分裂している。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 10.1.10.7 utf8mb4 文字セット (4 バイトの UTF-8 Unicode エンコーディング)

先程と同じように文字コードを指定して比較していく。

SET NAMES 'utf8' COLLATE 'utf8_unicode_ci';
select 'あ' = '?', '?' = '?';
# 0, 1

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';
select 'あ' = '?', '?' = '?';
# 0, 1

SET NAMES 'utf8' COLLATE 'utf8_general_ci';
select 'あ' = '?', '?' = '?';
# 0, 0

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';
select 'あ' = '?', '?' = '?';
# 0, 1

SET NAMES 'utf8' COLLATE 'utf8_bin';
select 'あ' = '?', '?' = '?';
# 0, 0

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_bin';
select 'あ' = '?', '?' = '?';
# 0, 0

SET NAMES 'utf8' COLLATE 'utf8_unicode_520_ci';
select 'あ' = '?', '?' = '?';
# 0, 1

SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_520_ci';
select 'あ' = '?', '?' = '?';
# 0, 0

// あれ、このあたり文字化けしてるかも…? 
// あ=絵文字1, 絵文字1=絵文字2; と絵文字を比較してます。。

むむ、結構厄介。絵文字が入力される可能性があり、ちゃんと識別したい場合は utfmb4_bin を使うのがよさそう。

参考リンク

utf8_unicode_ci に対する日本の開発者の見解 - かみぽわーる
MySQL と寿司ビール問題 - かみぽわーる

Laravel5 で Log ファサードを使ったときに一緒に標準出力にも出した

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

artisan コマンドを作ったとき、何かと様子を出力するために Log ファサードを使ってログファイルに出している。

 

ちなみに。そのとき Monolog のハンドラを使って、レベル別にファイルを分けたりなどしている。こんな具合にするとログレベルごとにファイルが分かれる、めっちゃ便利。

// bootstrap/app.php
$app->configureMonologUsing(function ($monolog) {
    $logConfigure = config('log');
    foreach ($logConfigure['file'] as $logLevel => $logFile) {
        $path = storage_path('logs/' . $logFile . '.log');
        $handler = new \Monolog\Handler\RotatingFileHandler(
            $path,
            $logConfigure['rotate'],
            $logLevel,
            false,
            $logConfigure['permission']
        );
        $handler->setFormatter(new Monolog\Formatter\JsonFormatter());
        $monolog->pushHandler($handler);
    }
});


// config/log
return [
    'permission' => 0777,
    'rotate' => 30,
    'file' => [
        \Monolog\Logger::DEBUG     => 'debug',
        \Monolog\Logger::INFO      => 'debug',
        \Monolog\Logger::NOTICE    => 'debug',
        \Monolog\Logger::WARNING   => 'warning',
        \Monolog\Logger::ERROR     => 'error',
        \Monolog\Logger::CRITICAL  => 'error',
        \Monolog\Logger::ALERT     => 'error',
        \Monolog\Logger::EMERGENCY => 'error',
    ],
];

自分で用意する artisan コマンド、ようはバッチでは様子の出力などをファイルに出すために Log::info(); とかして、いろいろな情報をログに出ていく。この ID のデータをやるよー、とか、おわったよー、とか、色々。

しかし開発中はそれって厄介で、パッとコマンド実行してログ見て。いや、横で tail -f とかしておけば良いのだろうけど、それはそれでちょっと面倒だなあ。というか artisan コマンドって verbose なオプションをサポートしているので、それをうまく使ってログに出すと同時に標準出力にも出せないだろうか。

と思って出来たのがコレ。

namespace App\Console;

use Illuminate\Console\Command;
use Monolog\Handler\StreamHandler;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

trait LogWithStdout
{
    /**
     * execute メソッドに割り込み Monolog の設定をする
     */
    public function execute(InputInterface $input, OutputInterface $output)
    {
        // verboseなオプションが付いたらログを画面に出す
        if ($output->getVerbosity() >= $this->verbosityMap['v']) {
            $monolog = \Log::getMonolog();
            $handler = new StreamHandler("php://stdout", 'info');
            $monolog->pushHandler($handler);
        }

        // 元の処理の呼び出し
        return parent::execute($input, $output);
    }
}
namespace App\Console\Commands;

use App\Console\LogWithStdout;
use Illuminate\Console\Command;

class FetchTrafficSummary extends Command
{
    use LogWithStdout;

    public function handle()
    {
        Log::info('start!');

        ...

        Log::info('complete!');
    }
}

本当はサービスプロバイダにしてイベントにしてあげたほうがキレイなのだが、 Laravel の実装上、それは出来ないっぽい…。うぬ。。

具体的にはコマンドの発火時は取れるのだが verbose オプションがあるかどうかが取れない。 OutputInterface がイベントに入ってこないのだ。ソースも追いかけてみたがイベント引数に入ってくるのは Application インスタンスだけ。 OutputInterface を持ったものは Application から Command を呼ぶときにしか作られないらしい。

 
 
とまあ、作ってみたものの Artisan コマンドでも $this->info などして標準出力に出せるので、そちらをオーバーラップしたほうがキレイなんじゃないかなーと思った。こういうイメージ。

public function info($string, $verbosity = null)
{
    parent::info($string, $verbosity);
    \Log::info($string);
}

どちらかといえばこちらのほうが、メソッド別にログファサードのメソッドを呼べるのでパッと見でわかりやすそうだ。

TF-IDF ってのを使うと単語の重要度がわかるよって聞いたので調べた

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

TF-IDF は文章における、単語の重みづけを行うもの。
処理した文章中のうち、ある単語はどの程度重要なものか、出現度合いから重み付けを計算する。
プログラム、機械からは文章の分析っていってもよくわからないので、そのよくわからない特徴をわかるようにするための、数値化する方法の1つ。

前提として TF-IDF には複数の文章を入力する必要がある。

TF-IDF = TF * IDF

TF = 1つの文章において、ある単語の出現回数 / 文章内の単語数
→ 文章1つずつの単語の出現頻度がわかる。

IDF = log( 文章数 / ある単語が出現する文章数 ) + 1
→ 横断的に使われる単語は低い値になる。

簡単な文章を入れつつ実際の数字を計算してみる。

 
step1:文章の入力

文章
リンゴとレモンとレモン
リンゴとミカン

 
step2:単語に分割(ここでは形態素解析し、名詞だけに絞ったとする)

文章 分割した結果
リンゴとレモンとレモン リンゴ レモン レモン
リンゴとミカン リンゴ ミカン

 
step3:TF値、IDF値を計算する

文章 TF:リンゴ TF:レモン TF:ミカン
リンゴとレモンとレモン 1 / 3 = 0.33 2 / 3 = 0.67 0 / 3 = 0
リンゴとミカン 1 / 2 = 0.5 0 / 2 = 0 1 / 2 = 0.5
単語 IDF
リンゴ log(2 / 2) + 1 = 1
レモン log(2 / 1) + 1 = 1.3
ミカン log(2 / 1) + 1 = 1.3

 
step4:TF-IDFを計算する

文章 TF-IDF:リンゴ TF-IDF:レモン TF-IDF:ミカン
リンゴとレモンとレモン 0.33 * 1 = 0.33 0.67 * 1.3 = 0.87 0 * 1.3 = 0
リンゴとミカン 0.5 * 1 = 0.5 0 * 1.3 = 0 0.5 * 1.3 = 0.65

 

この結果からわかることとして。

  • 「リンゴ」は横断的に出現するので値が低くなる。
  • 「レモン」は文章1だけに多く使われるので値に特徴が強く出る。
  • 「ミカン」は文章2だけに使われているが、リンゴと同程度しか使われていないので、特徴が大きく出るわけではない。
  • この2つの文章を切り分けるには レモン の様子を見ればよいらしい。ということで、確かに単語の重要度がわかるよ、っていうのはあっているらしい。

ただ、全ての文章・全ての単語を利用するという計算ロジックの都合、文章を逐次増やして差分計算していくー、というのは出来なさそうなので、そういう用途はむりそうだなー。
例えばこの計算済み TF-IDF の結果に加えて「バナナとレモン」を追加したときに困っちゃうね、という話。この量なら良いけど、もっと文章量が増えてもっと単語量が増えると、毎回計算し直すコストがかかる。

Composer で指定したライブラリがインストールできないんだけど!!と言われたと

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

composer require コマンドでライブラリを入れようとすると「ライブラリをいれたいけど入れられないよ!」なんて怒られることがある。いや、 composer install でも起こることあるわ。

起きたこと

※既に他のライブラリを入っている状態

$ composer require --dev "phpdocumentor/phpdocumentor:^2.8"
You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug
./composer.json has been updated

Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.
 
  Problem 1
    - phpdocumentor/phpdocumentor v2.8.0 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.1 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.2 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.3 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.4 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - phpdocumentor/phpdocumentor v2.8.5 requires herrera-io/phar-update 1.0.3 -> satisfiable by herrera-io/phar-update[1.0.3].
    - Conclusion: don't install herrera-io/phar-update 1.0.3
    - Installation request for phpdocumentor/phpdocumentor ^2.8 -> satisfiable by phpdocumentor/phpdocumentor[v2.8.0, v2.8.1, v2.8.2, v2.8.3, v2.8.4, v2.8.5].
 
 
Installation failed, reverting ./composer.json to its original content.

原因

Composer は依存ライブラリの解決を行う際、それぞれのライブラリの composer.json に記載されているバージョンを解釈し、お互いに問題のない、なるべく最新のものを選択しようとします。

このとき「あるライブラリではAの1.0.* を使います。別のあるライブラリではAの2.0.*を使います」といった記述がされていると、エラーになってしまうようです。

あるいは composer.lock としてバージョンが固定されてしまうときにも同様の問題が起こります。依存を再解決しようとするときに上記のようにぶつかってしまうことがあるようです。

対応策

順番に確認していくと良さそう。

  1. そもそも composer.json で書かれているバージョンとエラーになっているバージョンとが噛み合わない
    • → 使っているライブラリ、または入れようとしているライブラリのバージョンを見直す
  2. そんなことはない、依存していたんだ、というくらいのものでエラーが出てる
    • → composer.lock を消して composer install

どちらにしても、依存ライブラリのバージョンが変わるはずなので、テストの実行や動作確認を行ったほうがよいです。

Composer は何者か。あるいは install と update の違い。そしてオートロードの仕組み。

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

たいとるそのまま。

Composer 使って Packagist から ライブラリを入れられるけど結局何者なんだ、 install と update の違いはなんだ、つーかオートロードってどうなってんねん、などなどがよくわからなかったのでドキュメントとかソースを見た。

 

Composer とは?

Introduction - Composer

このあたりを、見ると大体解決する。

Composer "Dependency Manager for PHP"
→ PHP向けの依存関係管理システム

Ruby における Bundler や Node.js における npm のように、 PHP に対しての Composer 。

Composer 自体も PHP で書かれていて、PHP 5.3.2 以降の環境ならうまく動いてくれる。
場合によっては、コンパイルオプションの指定やモジュールの追加、コンフィグの問題などが発生するが、 Composer が判別し、警告を出してくれるのでその時に対応すれば良い。

 

依存関係管理?

作っていく PHP アプリケーション(あるいは PHP ライブラリ)が、ほかのライブラリに「依存」している場合、依存しているライブラリもセットにしないと動作しない。

仮にそのライブラリがまた別のライブラリに「依存」していたら…?
あるいは、ライブラリ A と別のライブラリ B が、ある同じライブラリ C に依存しているけど、 C と C' のように違うバージョンが対象だったら…?

などなどの非常にやっかいなライブラリ依存関係を、簡単な記述で管理できるようにしたものが依存関係管理システム。

PHP なら PEAR がある。が、今この時代では Composer のほうがよく使われている…はず。

 

Composer インストール

composerのインストールは非常に簡単。

$ curl -sS https://getcomposer.org/installer | php

この場合はカレントディレクトリに comopser.phar というファイルが作られ、これがComposerの本体になる。

どのディレクトリからでも実行できるように、どのユーザでも実行できるようにするためにはシステムにインストールする必要がある。

その際には、以下のコマンドで。

$ sudo mv composer.phar /usr/local/bin/composer
$ sudo chown root:root /usr/local/bin/composer
$ sudo chmod +x /usr/local/bin/composer

 

Composer がすること

  • composer コマンドの提供
  • 依存するライブラリについて
  • 依存関係の解決(ダウンロード&展開)
  • ライブラリ検索
  • 依存関係の追加
  • 依存関係の除去
  • オートロード用ファイルの提供

 

composer コマンドの提供

Composer をインストールすると composer というコマンドが利用できる。

※リネームしていない場合 composer.phar が実行コマンド。
実行権限が無い場合は php composer.phar XXXX のように php コマンドへの入力として実行する必要あり。

composer コマンドには、おおざっぱに以下の機能がある。

  • init
    • カレントディレクトに新しく composer.json を生成する
  • require
    • 依存関係を追加する
  • remove
    • 依存関係を削除する
  • install
    • composer.json に基づいて依存関係を解決する
    • composer.lock というファイルが存在する場合は、それに従う。
  • update
    • composer.json に基づいて依存関係を再度解決し composer.lock ファイルを更新する
  • dump-autoload
    • オートロード用ファイルを生成する
  • search
    • ライブラリを Packagist から探す
  • self-update
    • composer コマンド自体を更新する

他にも細かいところがあるので、気になったらドキュメントへ。
Command-line interface / Commands - Composer

 

composer install と composer update の違い

install

  • 7文字
  • composer.lock があればそれに従ってライブラリを導入する
  • composer.json に従ってライブラリを導入する

update

  • 6文字
  • composer.lock があっても composer.json に従ってライブラリを導入する

composer.lock は実際にインストールされているライブラリ、バージョンが記載されている。
composer.json には、このアプリケーション(ライブラリ)が一体どのライブラリ、どのバージョンに依存しているかを記載する。そのときライブラリのバージョンアップを形容できる書き方がある。

 

composer.json におけるバージョン指定の方法

詳しくはドキュメントにまとまってる。
Versions and constraints - Composer

ざっくりまとめると。

  • 固定値
    • 1.0.2
  • 範囲指定
    • > >= < <= != ||
    • 記号は書いたそのままの意味、|| は OR 。スペースで AND 。
    • >=1.0
      • 1.0 以上
    • >=1.0 <2.0
      • 1.0 以上 かつ 2.0 未満
    • >=1.0 <1.1 || >=1.2
      • (1.0 以上 かつ 1.1 未満)または 1.2 以上
  • ハイフンの範囲指定
    • 1.0 - 2.0
      • >=1.0.0 <2.1 と等価
      • 1.0 以上 かつ 2.1 未満
  • ワイルドカード
    • 1.0.*
      • >= 1.0 <1.1
      • 1.0 以上 かつ 1.1 未満
  • チルダ
    • 指定した一番小さいバージョンの変化を形容する
    • ~1.2
      • >= 1.2 <2.0
    • ~1.2.3
      • >= 1.2.3 <1.3
  • ハット
    • セマンティックバージョニングに従う = メジャーバージョンを上げない
    • ただし 1.0 未満はマイナーバージョンに合わせる
    • ^1.2.3
      • >=1.2.3 <2.0.0
    • ^0.3
      • >=0.3.0 <0.4.0

チルダ + マイナーバージョン指定にしておけば、大きな変更に殺されることはなさそう。

 

Composer の提供するオートローダーとは

composer dump-autoload を実行すると、 composer.json の内容にもとづいてオートロード用のファイルが作られる。ファイルは複数あり、以下の場所で確認できる。

  • vendor/autoload.php
  • vendor/composer/*

これらのファイルは次のような仕組みで成り立っている。ソースにコメントをつけて流れを見ていく。

<?php
// vendor/composer/autoload_real.php を読み込み、そちらに処理が移る。

require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d::getLoader();

vendor/composer/autoload_real.php

<?php
// 余談だけどこのハッシュ値っぽいものは指定できる。
// composoer config に autoloader-suffix があればそれで、なければ md5(uniqid('', true)) が使われる。
//
// composer.json に書くなら config > autoloader-suffix になるようにする。
// composer config autoloader-suffix HogeeeHugaaa でも可。
class ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d
{
    // ClassLoader インスタンス
    private static $loader;

    // spl_autoload_register によって利用される ClassLoader を読み込む
    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }

    public static function getLoader()
    {
        if (null !== self::$loader) {
            return self::$loader;
        }

        // ClassLoader の読み込み
        spl_autoload_register(array('ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        spl_autoload_unregister(array('ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d', 'loadClassLoader'));

        // PHP で、かつ Zend Guard Loader の拡張が有効なら autoload_static.php を利用する
        // 余談)Zned Guard Loader は Zned Optimizer によって作られたエンコードされた PHP ファイルを実行するためのもの
        // http://www.zend.com/en/products/guard/zend-optimizer-zend-loader
        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
        if ($useStaticLoader) {
            require_once __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInitaf2b1a382e2c643bceff8e5e7a5cce1d::getInitializer($loader));
        } else {
            // 条件に該当しなかった場合は、各ファイルを個別に読み込む

            // 名前空間の定義がされたものを読み込み
            $map = require __DIR__ . '/autoload_namespaces.php';
            foreach ($map as $namespace => $path) {
                $loader->set($namespace, $path);
            }

            // PSR-4 形式で定義されたものを読み込み
            $map = require __DIR__ . '/autoload_psr4.php';
            foreach ($map as $namespace => $path) {
                $loader->setPsr4($namespace, $path);
            }

            // 直にクラスが指定されているものを読み込み
            $classMap = require __DIR__ . '/autoload_classmap.php';
            if ($classMap) {
                $loader->addClassMap($classMap);
            }
        }

        // 読み込んだファイル群を spl_autoload_register に登録する
        $loader->register(true);

        return $loader;
    }
}

vendor/composer/ClassLoader.php (抜粋)

// spl_autoload_register を使って登録する
public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}


// spl_autoload_register によって呼ばれるメソッド
// ファイルが見つかれば include する
public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }
}


// オードロード要求のクラスが含まれるファイルを探す
public function findFile($class)
{
    // classmap ファイルから読み込んだリストにいれば、それを返す
    if (isset($this->classMap[$class])) {
        return $this->classMap[$class];
    }

    // 「ファイルが見つからなかった」がキャッシュされていれば当然見つからない
    // あるいは dump-autoload のときに classmap-authoritative が指定されていれば classmap だけにする
    if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
        return false;
    }

    // APCu による最適化が有効になっていれば APCu から読み込めるか試みる
    if (null !== $this->apcuPrefix) {
        $file = apcu_fetch($this->apcuPrefix.$class, $hit);
        if ($hit) {
            return $file;
        }
    }

    // クラス名 .php のファイルを探す
    $file = $this->findFileWithExtension($class, '.php');

    // HHVM で動いているなら .hh ファイルを探す
    if (false === $file && defined('HHVM_VERSION')) {
        $file = $this->findFileWithExtension($class, '.hh');
    }

    // APCu 最適化が有効なら APCu にキャッシュ登録する
    if (null !== $this->apcuPrefix) {
        apcu_add($this->apcuPrefix.$class, $file);
    }

    // 「ファイルが見つからなかった」のキャッシュ
    if (false === $file) {
        // Remember that this class does not exist.
        $this->missingClasses[$class] = true;
    }

    return $file;
}


// クラス名 + 拡張子 のファイルを探すメソッド
private function findFileWithExtension($class, $ext)
{
    // PSR-4 形式のファイルを探す
    $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

    $first = $class[0];
    if (isset($this->prefixLengthsPsr4[$first])) {
        $subPath = $class;
        while (false !== $lastPos = strrpos($subPath, '\\')) {
            $subPath = substr($subPath, 0, $lastPos);
            $search = $subPath.'\\';
            if (isset($this->prefixDirsPsr4[$search])) {
                foreach ($this->prefixDirsPsr4[$search] as $dir) {
                    $length = $this->prefixLengthsPsr4[$first][$search];
                    if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
                        return $file;
                    }
                }
            }
        }
    }

    // PSR-4 のフォールバック
    // ※ 名前空間 => ディレクトリ名 という指定をしたもの
    foreach ($this->fallbackDirsPsr4 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
            return $file;
        }
    }

    // PSR-0 形式のファイルを探す
    if (false !== $pos = strrpos($class, '\\')) {
        // namespaced class name
        $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
            . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
    } else {
        // PSR-0 は PEAR 形式のファイル名も形容するので、それを探す
        $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
    }

    if (isset($this->prefixesPsr0[$first])) {
        foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
            if (0 === strpos($class, $prefix)) {
                foreach ($dirs as $dir) {
                    if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                        return $file;
                    }
                }
            }
        }
    }

    // PSR-0 のフォールバック
    // ※ プリフィックス => ディレクトリ名 という指定をしたもの
    foreach ($this->fallbackDirsPsr0 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
            return $file;
        }
    }

    // PSR-0 のインクルードパスの解決
    if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
        return $file;
    }

    // 見つからねーもんは見つからない。
    return false;
}


// 指定したファイルを include する
// ※これだけグローバル空間にいる
// ※includeしたものから $this や self を利用できないように、のため
function includeFile($file)
{
    include $file;
}

で、autoload_real から 読み込まれるファイルがいくつかあるけどなんやねん、という話は composer.json に書く。書き方はドキュメントに詳しくある。

The composer.json Schema - Composer

ざっくりとまとめると。

  • PSR-4
    • 名前空間でディレクトリを切る
    • プリフィックス : ディレクトリ名 として名前空間を設定できる
  • PSR-0
    • 名前空間でディレクトリを切る
    • プリフィックス : ディレクトリ名 として名前空間を設定できる
    • PSR-4 との違いは アンダースコアでも切ることができる、名前空間なしファイルが存在すること
  • classmap
    • 名前空間など PSR-0 または PSR-4 に従っていないもの
    • クラスのあるディレクトリやファイルを直に指定する
  • file
    • 細かいこととかいいから、俺はこのファイルを読み込みたいんだ

こんな感じ。 PSR-0 と PSR-4 はざっくりだけど、詳しく書くともっと長くなるのでまた。

 

オートローダーの最適化

このあたりのざっくりまとめ。

Autoloader Optimization - Composer

特にオプションなしに composer install または composer update 。 あるいは composer dump-autoload を実行すると、上記のまま処理が動く。

多数のライブラリを読み込むような巨大なフレームワーク、アプリケーションなどは、特に PSR-0 や PSR-4 のディレクトリ構成を解析して読み込む件で時間がかかってしまう。そこで、事前にオートローダーを最適化することができる。 -o オプションをつけるとよい。

$ composer dump-autoload -o

これで PSR-0 も PSR-4 も全てのオードロードの記述が classmap に変換される。 classmap になることで、クラス名からディレクトリを探しにいくことがなくなりその分高速化が見込める。

 
さらに本番環境など、ソースがもりもり変わることがなければ -a というオプションも付けるとよい。上のソース解説で出てきた classmap-authoritative のくだり。

$ composer dump-autoload -o -a

このオプションがあれば classmap に見つからないときに処理を諦める。そもそも composer dump-autoload なしに本番のソースが変わることはないだろうので、つけたほうが良い。が、可能性として、クラスが見つかりません!になってしまうことがある。

 
何かしらの都合で -a オプションはだめだー!となるなら APCu を有効にする戦略も取れる。 -a に比べて遅くはなるが、クラスが見つかりません!問題に対応しやすくなる。とはいえ APCu はメモリに乗っかってくるので、メモリに余裕があるような環境じゃないと使えない。

$ composer dump-autoload -o -apcu

上のソースを見てわかるとおり classmap-authoritative と APCu は同居しない、どちらかしか使えない。


複雑に絡み合ったひもがちょっとずつ解けていくように Composer の動きに詳しくなっていく気持ちがする。

が、ちょっとつかれたのでここでおちまい。

算術平均を逐次計算したい。

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

数式をこねくりまわす知識や技術はないので自分で求められないのですが、平均値って逐次計算できるのかな?を調べたメモ。

x = [...] // 元の数列
z = ...   // 追加したい数

// 平均値
Average(x) = Sum(x) / Count(x)

// 数を追加した平均値
Average(x + z) = Average(x) + ((z - Average(x)) / Count(x))

調べていたらこういう式も。平均に個数をかけて数を足し、そこから新しい個数で割れば、新しい平均だよね、というもの。そりゃそうだわ。

Average(x + z) = (Average(x) * Count(x) + z) / Count(x + z)

どちらにしても個数と平均値だけ持っておけば逐次計算できますね。後者は掛け算するので、個数や平均値が大きすぎると扱えなさそうなので、前者のほうがよさそう。

試しに実装してみるとこういう形。

class MeanStream
{
    private $mean = 0;
    private $dataCount = 0;

    public function __construct() {
    }

    public function add($num) {
        $this->dataCount++;
        $this->mean = $this->mean + (($num - $this->mean) / $this->dataCount);
        return $this;
    }

    public function get() {
        return $this->mean;
    }
}

echo new MeanStream()
    ->add(1)
    ->add(7)
    ->add(8)
    ->add(6)
    ->add(2)
    ->add(5)
    ->add(1)
    ->add(8)
    ->add(9)
    ->add(4)
    ->get();

echo (1+7+8+6+2+5+1+8+9+4) / 10;
// 5.1

あってますねー

 
ちなみに、当然のことながら上記の例だと浮動小数点数を使うので、使う値によっては表現の都合で微妙に数値が変わるのでその点だけ注意ですね。

$values = [];
for($i = 0; $i < 10; $i++) {
    $values[] = mt_rand();
}

$mean = new MeanStream();
foreach ($values as $value) {
    $mean->add($value);
}

echo sprintf('%.30f', array_sum($values) / count($values));
// 995988838.500000000000000000000000000000

echo sprintf('%.30f', $mean->get());
// 995988838.499999880790710449218750000000

PHP じゃない言語で実装するとして、もし Decimal のような、よろしく精度をもってくれる機能があればそれを使ってあげるほうがよさそうです。PHP は言語として標準に Decimal がないのでどうするんだろう、 bcmath とか gmp というのがありそうだけど extension に左右されるなー。まあ composer.json で extension について依存あるよーと記載すればいいのか。
PHP: 数学 - Manual

Packagist みると幾つかありそうなので、このあたりを使ったほうがいいのかなー。

 
と、話がそれてきたあたりでおしまい。ライブラリの話はまた今度。

1 2 3 4 5 6 7 8 9 10 11 12 13