読者です 読者をやめる 読者になる 読者になる

超PHPerになろう

Enjoy PHP Programming

オブジェクトをいい感じに複製(クローン)する [myclabs/deep-copy]

「オブジェクトの複製」には本質的に厄介な問題をいくつも含みます。特に、オブジェクトの再帰的な複製(ディープコピー)には直感的ではない動作や単純ではない依存関係が発生しがちです。myclabs/deep-copyはそれをいい感じに解決してくれます。

公式サイト myclabs/DeepCopy: Create deep copies (clones) of your objects
概要 Create deep copies (clones) of your objects
パッケージ名 myclabs/deep-copy
作者 My C-Labs
mnapoli (Matthieu Napoli)
ライセンス MIT License
バージョン v1.5.1 (2015-11-20) Packagist

インストール

Composerでインストール可能です。

composer.phar require myclabs/deep-copy

配列のコピー

文字列および配列を含む全てのPHPの値と変数はコピーオンライト(CoW)と呼ばれる最適化戦略がとられます。

<?php

$a = ['a', 'b', 'c'];
$b = $a;
var_dump($a === $b);
//=> true

$a[1] = 'X';
$b[] = 'ZZZ';
var_dump($a);
//=> ["a", "X", "c"]
var_dump($b);
//=> ["a", "b", "c", "ZZ"]
var_dump($a === $b);
//=> false

$b = $aが実行された状態では両者は同じ配列ですが、破壊的操作を行ったタイミングで別々の道を歩み始めます。つまり、二つの配列を複製したければ、PHPでは別の変数に代入してやるだけで十分なのです。

一方でRubyは、このような振舞はしません。

a = ['a', 'b', 'c']
#=> ["a", "b", "c"]
b = a
#=> ["a", "b", "c"]

a[1] = 'X'
b.push = 'ZZZ'

p a
#=> ["a", "X", "c", "ZZZ"]
p b
#=> ["a", "X", "c", "ZZZ"]
p a == b
#=> true

このように、Rubyでは単に別の変数に代入しただけでは値が複製されたことにはならず、終始において一蓮托生です。

オブジェクトのコピー

次に、stdClassを使ってオブジェクトのコピーについて実験してみます。

<?php

$book = new \stdClass;
$book->name = "共産党宣言";
$book->authors = ["カール・マルクス"];

$book2 = $book;

var_dump($book === $book2);
//=> bool(true)

$book2->authors[] = "フリードリヒ・エンゲルス";
$book->authors[0] = "Karl Heinrich Marx";

var_dump($book === $book2);
//=> bool(true)

配列と同じようにやったつもりですが、違った結果になりました。

PHPでオブジェクトを複製するには別の変数に代入するだけでは不十分で、$copy = clone $obj;のようにcloneキーワードを用ゐる必要があります。このことはPHP: オブジェクトのクローン作成 - Manualに説明があります。

再帰的なオブジェクトのコピー

ここまででオブジェクトの複製について基本的な理解が得られたところで、ここからはGitHubmyclabs/DeepCopyのREADMEから画像を拝借しつつ説明を進めていきます。

アルファベット1文字のクラスでとてもわかりにくいのですが、以下のようなクラスがあったとします。

<?php

class A
{
    /** @var string */
    public $name;
    /** @var B */
    public $b;
    /** @var C */
    public $c;

    public function __construct($name)
    {
        $this->name = "Hi, I am {$name}.";
    }
}

class B
{
    /** @var string */
    public $name;
    /** @var C */
    public $c;

    public function __construct($name)
    {
        $this->name = "Hi, I am {$name}.";
    }
}

class C
{
    /** @var string */
    public $name;

    public function __construct($name)
    {
        $this->name = "Hi, I am {$name}.";
    }
}

これらのクラスを使ってコードを書いてみますね。

<?php
$c = new C('Charlie');

$b = new B('Bob');
$b->c = $c;

$a = new A('Alice');
$a->c = $c;
$a->b = $b;

これをグラフにすると以下のような状態です。

f:id:zonu_exe:20160811234717p:plain

次に、これを「まるごと」複製してみたいと思ったとします。まるごと複製とは、ここでは$a$a2に複製したとして、$a2->cのオブジェクトを変更したとしても$a->cのオブジェクトには反映されたくないといふことだとします。$a->bについても同様。

<?php

$a2 = clone $a;
// => A {#192
//      +name: "Hi, I am Alice.",
//      +b: B {#200
//        +name: "Hi, I am Bob.",
//        +c: C {#173
//          +name: "Hi, I am Charlie.",
//        },
//      },
//      +c: C {#173},
//   }

ほうほう。では$a2->c-nameに別の文字列を入れてみたらどうかな。

<?php

$a2->c->name = 'Charlotte';
// => "Charlotte"
$a
// => A {#178
//      +name: "Hi, I am Alice.",
//      +b: B {#200
//        +name: "Hi, I am Bob.",
//        +c: C {#173
//          +name: "Charlotte",
//        },
//      },
//      +c: C {#173},
//    }

$a2
// => A {#192
//      +name: "Hi, I am Alice.",
//      +b: B {#200
//        +name: "Hi, I am Bob.",
//        +c: C {#173
//          +name: "Charlotte",
//        },
//      },
//      +c: C {#173},
//    }

$a->cの方も変っちゃったよだめじゃん…

これはグラフにすると以下のような状態です。

f:id:zonu_exe:20160812002853p:plain

__clone()大戦

今度もPHP: オブジェクトのクローン作成 - Manualを参考に__clone()マジックメソッドを定義してみることにします。またクラス定義ですが、変更点は__clone()が増えてるだけです。

<?php

class A
{
    /** @var string */
    public $name;
    /** @var B */
    public $b;
    /** @var C */
    public $c;

    public function __construct($name)
    {
        $this->name = "Hi, I am {$name}.";
    }

    public function __clone()
    {
        $this->b = clone $this->b;
        $this->c = clone $this->c;
    }
}

class B
{
    /** @var string */
    public $name;
    /** @var C */
    public $c;

    public function __construct($name)
    {
        $this->name = "Hi, I am {$name}.";
    }

    public function __clone()
    {
        $this->c = clone $this->c;
    }
}

class C
{
    /** @var string */
    public $name;

    public function __construct($name)
    {
        $this->name = "Hi, I am {$name}.";
    }
}

よし、これなら動くかな…?

<?php

$a2 = clone $a;
$a2->c->name = 'Charlotte';
// => "Charlotte"
$a
// => A {#209
//      +name: "Hi, I am Alice.",
//      +b: B {#218
//        +name: "Hi, I am Bob.",
//        +c: C {#213
//          +name: "Hi, I am Charlie.",
//        },
//      },
//      +c: C {#213},
//    }

// >>> $a2
// => A {#207
//      +name: "Hi, I am Alice.",
//      +b: B {#199
//        +name: "Hi, I am Bob.",
//        +c: C {#210
//          +name: "Hi, I am Charlie.",
//        },
//      },
//      +c: C {#197
//        +name: "Charlotte",
//      },
//    }

今度は$a->c$a2->cの状態はきっちり切り離せたようです。しかし今度は$a->c$a->b->cの関係が奇妙なことになってしまひましたね。

f:id:zonu_exe:20160812002807p:plain

やれやれ…

myclabs/deep-copyの出番だ

__clone()では問題が解決しないことがわかったので、クラス定義は元の状態に戻します。

<?php

use DeepCopy\DeepCopy;

$c = new C('Charlie');

$b = new B('Bob');
$b->c = $c;

$a = new A('Alice');
$a->c = $c;
$a->b = $b;

// Deepcopyを使ってオブジェクトのコピー
$deepCopy = new DeepCopy();
$a2 = $deepCopy->copy($a);

$a2->c->name = 'Charlotte';
// => "Charlotte"
$a
// => A {#190
//      +name: "Hi, I am Alice.",
//      +b: B {#192
//        +name: "Hi, I am Bob.",
//        +c: C {#186
//          +name: "Hi, I am Charlie.",
//        },
//      },
//      +c: C {#186},
//    }

$a2
// => A {#188
//      +name: "Hi, I am Alice.",
//      +b: B {#165
//        +name: "Hi, I am Bob.",
//        +c: C {#202
//          +name: "Charlotte",
//        },
//      },
//      +c: C {#202},
//    }

やった、ばっちりだ!

f:id:zonu_exe:20160812005805p:plain

このライブラリは利用すべきなの?

オブジェクトの複製にどのような手法をとるのが望ましいかは、場合によります。そもそも再帰的な複製(ディープコピー)は必要なく、浅い複製(シャローコピー)で十分だといふケースもあります。myclabs/deep-copyは直感に反しないディープコピーの手法として、良い選択肢のひとつです。

上記のようなオブジェクトの複製に伴った理窟がいまひとつ理解できない場合は、オブジェクトの利用を諦めて連想配列に落としてみるといったこともPHPらしい、割り切った解決法ではあります。筆者が業務として開発に携るpixiv.netでは、オブジェクトはほとんど利用しません。

すごいPHPerになるためにいろいろやってた

すごいPHPer(ぺちぱー)になりたいと思っていろいろやってたんですが、ちょっと慌しいのが重なって報告わすれてました ヾ(〃><)ノ゙☆

PHP BLT #3でしゃべってきた

PHP BLT #2は参加できなかったけど、#3には参加してました ヾ(〃><)ノ゙

PhpStormはすごいんですけど、Emacsの実力もこんなもんじゃないんで、がんばっていきたい…!

インスパイヤされて掲示板を作りたくなってた

qiita.com

しばらく更新できてなかったけど、そろそろ再開できるかな…!

WEB+DB Press Vol. 92にHTTPの話を書いた

gihyo.jp

PHPからHTTPを叩く方法はいろいろあるんだけど、とりあへずguzzle/guzzle: Guzzle, an extensible PHP HTTP clientの6.xを使っとけばいいんじゃないの、みたいなことを書きました。

PHPカンファレンス北海道2016でComposerについてしゃべってきた

関係者から招待いただいたとかじゃなくて、インターネットで見掛けたのでCFPを送ったら通ってしまったので、喋らせていただいた次第です。

phpcon.sapporo-php.net

私は地元が北海道だったので、大学生じゃなくなってぷらぷらしてた時期にはRuby札幌とかPython札幌で活動してた頃もあるので、かなり懐かしい感じでしたね!

PHP BLT #4でしゃべってきた

この話に関してはQiitaあたりに、もうちょっと詳しく書きます!

適当なライブラリ

クラス名のエイリアスはるやつ

github.com

なんかファイルごとにuseとかするのめんどいじゃん? ってときに使へるやつですね。動作原理を解説してもいいんですけど、ずこーって感じがします。

クラスの擬似動的ロード/モンキーパッチをするやつ

二ヶ月くらゐ前に書いてGitHubに上げてた気がするんだけど、幻覚だったようだ… 動いてはあるので、近々ちょっと直して公開するかも。

この期間での知見

やりたいことなんでも手をひろげていった揚句にどれも完成度いまいち、みたいなのって割と最悪に近いので、ほどほどにがんばります ヾ(〃><)ノ゙

すごいPHPerになるためにオートローディングについて調べて記事書いた

Composerとかでインストールしたライブラリではなにげなく使ってる名前空間オートローディングについて詳しく書いてます。まあ、PHP: 名前空間 - Manualとか読めば書いてあるんですが、Composerとかオートローディングと組合せてきちんと説明してる記事はWebでもあんまりないと思ふので、会社での布教などにぜひご利用くださいヾ(〃><)ノ゙

gihyo.jp

PHP BLT #1で「たのしい独自フレームワーク」を発表しました

PHP BLT #1PHP界隈のTipsについて5分で話すLT大会です。

phpblt.connpass.com

コンセプトとしてはもっと気軽な内容を意図されてたはずなのですが、このスライドでは20分枠で話すような内容を説明不足に任せて駆け足でLTにした感じです!

www.slideshare.net

独自フレームワークの設計の具体的なところについては、このblogでもきちんと書いていきたいですね ヾ(〃><)ノ゙

User-Agent文字列を解釈するには [woothee/woothee]

User-Agent/UA文字列はブラウザなどのユーザーエージェントが自己申告する識別子のことです。この値は歴史的経緯から複雑怪奇で、付け焼刃の実装では正確な判定が困難です。

Project Wootheeプログラミング言語に依存しないUA文字列パーサーを開発するプロジェクトです。言語を横断してテストを共有して居り、同じバージョンを利用すれば、どの言語実装を利用しても同じ結果が得られることが保障されます。

公式サイト Woothee: User-Agent parser/classifier for multi languages by woothee
概要 Cross-language UserAgent classifier library, PHP implementation
パッケージ名 woothee/woothee - Packagist
作者 tagomoris (TAGOMORI "moris" Satoshi)
yuya-takeyama (Yuya Takeyama)
ライセンス Apache License, Version 2.0
バージョン v1.2.0 (2015-08-14)

インストール

Composerでインストール可能です。

composer.phar require woothee/woothee

UA文字列とは

以前、勉強会で発表するために作成したスライドを紹介します。

www.slideshare.net

ブラウザ業界は常に渾沌として居り、現在はMozilla Firefoxを除く多くのブラウザは「WebKitだ」と名乗ります^1

# Chrome 
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2560.0 Safari/537.36
# Mozilla Firefox
Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:42.0) Gecko/20100101 Firefox/42.0
# Safari
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/601.2.7 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.7
# Microsoft Edge
Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586
# Opera(OPR)
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36 OPR/33.0.1990.58

これらの変化する情勢について自前で解釈するのは不毛なので、WootheeのようなUAパーサーを利用することは有効です。

UAを分岐する目的

とは言っても現在の多くのブラウザの挙動は標準化されて居り、サーバーサイドのレベルでブラウザの種類そのものを判定することが必要な処理は、ほとんどありません。

実用例を挙げるならば、次のような用途があります。

  • UAスマートフォンかどうかを判定し、モバイル向けサイトにリダイレクトする
  • セキュリティを目的に、著しく古いブラウザ(IE8以下など)での利用を抑制する
  • TwitterFacebookクローラーなどに、TwitterカードOGPを出力する
  • OSごとに異なるファイルをダウンロードさせる

それ以外の目的で不用意に判定を追加すると、将来登場する新しい環境で意図しない挙動をするおそれがあるので、本当に必要な処理かどうか検討してください。

また、GooglebotなどのUAに対して通常のブラウザと著しく異なるレスポンスを返した場合、検索結果に想定できない悪影響がある場合があります。単にGooglebotからのアクセスを拒否することは問題ありませんが、その場合はrobots.txtを利用することを推奨します。詳細はGooglebot - Search Console ヘルプを参照ください。

UAの取得方法

標準のPHPでは、以下のような方法でUser-Agent文字列を取得できます。

<?php
$UA = isset($_SERVER['HTTP_ USER_AGENT']) ? $_SERVER['HTTP_ USER_AGENT'] : '';

$woothee = new \Woothee\Classifier;
$result = $woothee->parse($UA);

ただし、Webフレームワークを利用する場合はテスタビリティを確保するため、フレームワーク推奨の方法で取得しましょう。

以下はLaravelのファサードを利用した記法です。

<?php
$UA = Request::server('HTTP_USER_AGENT');

$woothee = new \Woothee\Classifier;
$result = $woothee->parse($UA);

解析結果

本稿執筆時点でのWoothee 1.2.0では、以下のような出力が得られます。

<?php
$woothee = new \Woothee\Classifier;

# Twitterのクローラー
$woothee->parse("Twitterbot/1.0");
# => [
#     "name" => "twitter",
#     "category" => "crawler",
#     "os" => "UNKNOWN",
#     "version" => "UNKNOWN",
#     "vendor" => "UNKNOWN",
#     "os_version" => "UNKNOWN",
# ]

$woothee->parse("facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)");
# => [
#     "name" => "facebook",
#     "category" => "crawler",
#     "os" => "UNKNOWN",
#     "version" => "UNKNOWN",
#     "vendor" => "UNKNOWN",
#     "os_version" => "UNKNOWN",
# ]

$woothee->parse("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2560.0 Safari/537.36");
# => [
#     "name" => "Chrome",
#     "vendor" => "Google",
#     "version" => "48.0.2560.0",
#     "category" => "pc",
#     "os" => "Mac OSX",
#     "os_version" => "10.10.5",
# ]

$woothee->parse("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586");
# => [
#     "name" => "Edge",
#     "vendor" => "Microsoft",
#     "version" => "13.10586",
#     "category" => "pc",
#     "os" => "Windows 10",
#     "os_version" => "NT 10.0",
# ]

$woothee->parse('Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)');
# => [
#     "name" => "Internet Explorer",
#     "vendor" => "Microsoft",
#     "version" => "6.0",
#     "category" => "pc",
#     "os" => "Windows XP",
#     "os_version" => "NT 5.1",
# ]

$woothee->parse("Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)");
# => [
#     "name" => "Internet Explorer",
#     "vendor" => "Microsoft",
#     "version" => "8.0",
#     "category" => "pc",
#     "os" => "Windows UNKNOWN Ver",
#     "os_version" => "NT 5.2",
# ]

php.netのひみつ(1) 短縮URL

php.netPHPの公式サイトです。ダウンロードやリリースノートがあるのは当然なのですが、その中でもPHP: Documentationには技術文書が網羅されてます。

RubyPythonのドキュメントが各バージョンごとや英語とその他の言語(日本語を含む)の断片化が著しいのに対し、php.netはきちんと国際化されてます。

さて、あなたは関数/メソッドの仕様を知りたくなったとき、どうやってその文書まで辿りつくでしょうか。

例としてstrtotimeの日本語版記事のURLは http://php.net/manual/ja/function.strtotime.php です。こんな長いURLを打ち込むわけにもいかないのでGoogleなどの検索エンジンに頼るでしょうか。

それもひとつの選択肢ではありますが、ブラウザにphp.net/strtotimeと打ち込みます。

このようにphp.net/xxxxxのようなURLに対して、さまざまなショートカットが設定されてます。

ただし最近はDash for OS X - API Documentation Browser, Snippet Manager - Kapeliのようなドキュメントツールがあるので、そちらの話は後日また。

PHPとMIT Licenseについて

Webにはフリーソフトウェアとして公開された多くのPHPライブラリがあります。しかし、多くのスクリプトには「ライセンス」と呼ばれる利用条件が設定されて居り、利用者はこれを遵守する必要があります。

今回は多くのPHPライブラリのライセンスとして採用される MIT License について紹介します。

ライセンス名 The MIT License
SPDX Identifier MIT
分類 フリーソフトウェア / 寛容なライセンス
原文 MIT License - Choose a License
必須事項 ライセンスと著作権の表示
許可されること 商用利用、修正、配布、派生作品に別のライセンスを課す
禁止事項 作者に責任を求めること
同等のライセンス Expat License
BSD 2-Clause License

要点

MIT Licenseは「寛容なライセンス」であると呼ばれます。業務利用もしやすいライセンスで、利用者が遵守しなければならない条件も非常に少なく簡潔です。

  • ソースコードか添付文書に著作権表示とライセンスをぜったい記載してね (除去したらだめ)
  • このソフトを使って損害が生じても作者に責任はないからね (免責事項)
  • ↑ これを守ってくれる限り、無制限に使っても配ってもいいよ!

あなたが守らねばならないこと

改造して利用する場合

自由に改造して、再配布することができます。有料で販売したり、別の利用条件(ライセンス)を課しても問題ありません。

ただし、大前提となる著作権表示とライセンス文は必ず含まねばなりません

Composerで依存する場合

MIT Licenseが設定されたライブラリをComposerでrequireする場合、自作のソフトウェアの配布物の複製を含まず利用者に個別にダウンロードさせるため、特に何もすることはありません。READMEなどの文章に著作権表示や謝辞を記載する必要もありません。

業務で利用する場合

営利目的であっても問題なく業務利用できます。ただし、利用したPHPコードにバグがあり障害が発生したとしても、保障や損害賠償を要求することはできません。何か問題があれば自分で原因を調査して解決する覚悟を持って利用してください^1

サーバーで利用する場合

ソースコードを他者に配布せずサーバーでPHPを動作させて利用する場合、著作権表示やライセンス文を記載する必要はありません^2

JavaScriptを利用する場合

WebサービスでのJavaScriptソースコードを利用者にダウンロードさせるため、著作権表示を除去しない限りは何もせずに商用利用・再配布が可能です。

あなたがライブラリ作者の場合

  • あなたが作ったライブラリを利用したことが原因で問題が発生しても解決や賠償の義務はありません
  • サーバーに採用されたことを確実に知る手段はありません
  • 派生版を商用販売されたとき、確実に利益を受け取る手段はありません
    • 派生版のライセンスによっては改善をフィードバックできないことがあります
  • ビジネスのライバルに利用されることを阻止する方法はありません

ライセンス本文

非常に短いため、ライセンス全文を掲載します。

原文

Copyright (c) <year> <copyright holders>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

日本語訳

Open Source Group JapanによるMIT Licenseの日本語訳を紹介します。

Copyright (c) <year> <copyright holders>

以下に定める条件に従い、本ソフトウェアおよび関連文書のファイル(以下「ソフトウェア」)の複製を取得するすべての人に対し、ソフトウェアを無制限に扱うことを無償で許可します。これには、ソフトウェアの複製を使用、複写、変更、結合、掲載、頒布、サブライセンス、および/または販売する権利、およびソフトウェアを提供する相手に同じことを許可する権利も無制限に含まれます。

上記の著作権表示および本許諾表示を、ソフトウェアのすべての複製または重要な部分に記載するものとします。

ソフトウェアは「現状のまま」で、明示であるか暗黙であるかを問わず、何らの保証もなく提供されます。ここでいう保証とは、商品性、特定の目的への適合性、および権利非侵害についての保証も含みますが、それに限定されるものではありません。 作者または著作権者は、契約行為、不法行為、またはそれ以外であろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの使用またはその他の扱いによって生じる一切の請求、損害、その他の義務について何らの責任も負わないものとします。