Page 1
実用メタプログラミング
Practical PHP meta programming
PHPカンファレンス沖縄2019
2019-10-12 #phpconokinawa
公開日:
by USAMI Kenta @tadsan
に沖縄県宜野湾市のCODEBASE OKINAWAで開催された『PHPカンファレンス沖縄2019』でレギュラートーク(30分)として発表しました。
実用メタプログラミング
Practical PHP meta programming
PHPカンファレンス沖縄2019
2019-10-12 #phpconokinawa
お前誰よ
うさみけんた (@tadsan)
/ Zonu.EXE
さて
メタプログラミング
とは何か
そもそも
メタとは
出典: フリー百科事典『ウィキペディア(Wikipedia)』
https://ja.wikipedia.org/wiki/メタ 最終更新 2019年1月10日 10:01 版
出典: フリー百科事典『ウィキペディア(Wikipedia)』
https://ja.wikipedia.org/wiki/メタ 最終更新 2019年1月10日 10:01 版
出典: フリー百科事典『ウィキペディア(Wikipedia)』
https://ja.wikipedia.org/wiki/メタ 最終更新 2019年1月10日 10:01 版
メタプログラミング
とは何か
プログラム自身に
言及するような プログラミング
プログラムを
プログラミングする
どういうこと?
たとえば実行時に
関数を定義する
たとえば実行時に
実行する関数を決める
たとえばプロパティや
メソッドのアクセス
権限を制御する
たとえば実装コード
から自動でテスト コードを生成する
厳密にプログラミング とメタプログラミング を分けるものはないが
フレームワークは大な り小なりメタプログラ ミングを利用している
(ことが多い)
PHPの可能性を
広げるのが
メタプログラミング
メタプログラミング
できることで
有名な言語
Ruby
出典: O'Reilly Japan - メタプログラミングRuby 第2版
https://www.oreilly.co.jp/books/9784873117430/
Paolo Perrotta 著 / 角征典 訳 / 2015年10月 発行
Lisp
出典: フリー百科事典『ウィキペディア(Wikipedia)』
https://ja.wikipedia.org/wiki/メタプログラミング
最終更新 2017年12月6日 23:08 版
PHPではできない
ことの方が多い
オープンクラスがない
プリプロセスもない
PHPならではの 戦いかたがある!
関数
FizzBuzzで
考えてみましょう
出典: どうしてプログラマに・・・プログラムが書けないのか?
http://www.aoky.net/articles/jeff_atwood/why_cant_programmers_program.htm
Jeff Atwood著 / 青木靖 訳
出典: どうしてプログラマに・・・プログラムが書けないのか?
http://www.aoky.net/articles/jeff_atwood/why_cant_programmers_program.htm
Jeff Atwood著 / 青木靖 訳
FizzBuzz模範回答
<?php
foreach (range(1, 100) as $n) {
$s = '';
if ($n % 3 === 0) $s .= 'Fizz'; if ($n % 5 === 0) $s .= 'Buzz';
echo ($s === '') ? $n : $s, "\n";
}
FizzBuzz模範回答 関数分割
<?php
function to_fizzbuzz(int $n) {
$s = '';
if ($n % 3 === 0) $s .= 'Fizz'; if ($n % 5 === 0) $s .= 'Buzz';
return ($s === '') ? $n : $s;
}
foreach (range(1, 100) as $n) { echo to_fizzbuzz($n), "\n";
}
関数に分けるとテストが書ける
<?php
assert(to_fizzbuzz(1) === "1"); assert(to_fizzbuzz(2) === "2");
assert(to_fizzbuzz(3) === "Fizz");
assert(to_fizzbuzz(4) === "4");
assert(to_fizzbuzz(5) === "Buzz");
無茶振りをしましょう
関数に分けたい
でも引数使いたくない
愚直に書くとこうなる
<?php
assert(fizzbuzz1() === "1"); assert(fizzbuzz2() === "2");
assert(fizzbuzz3() === "Fizz");
assert(fizzbuzz4() === "4");
assert(fizzbuzz5() === "Buzz");
function fizzbuzz1() { return "1"; } function fizzbuzz2() { return "2"; }
function fizzbuzz3() { return "Fizz"; }
function fizzbuzz4() { return "4"; }
function fizzbuzz5() { return "Buzz"; }
手書きで
100個書くの!?
こういうことはできない
このコードは になります
<?php // syntax error
foreach (range(1, 100) as $n) {
$s = '';
if ($n % 3 === 0) $s .= 'Fizz'; if ($n % 5 === 0) $s .= 'Buzz';
変数を使った関数定義はできない
//
function fizzbuzz$n(){
return ($s === '') ? $n : $s, "\n";
}
}
[非推奨] evalを使った無茶
<?php
foreach (range(1, 100) as $n) {
$s = '';
if ($n % 3 === 0) $s .= 'Fizz'; if ($n % 5 === 0) $s .= 'Buzz';
$s = ($s === '') ? $n : $s;
を使えばいちおうできる
// eval
eval("function fizzbuzz{$n} (){
return '{$s}';
}");
}
evalはなんでも
できるがリスクも大きい
最後の手段
( 関数も
create_function()
あったがPHP7.2で非推奨)
無名関数
(クロージャ)
を使いましょう
クロージャを使えばできる
<?php
$fizzbuzz = [];
foreach (range(1, 100) as $n) {
$s = '';
if ($n % 3 === 0) $s .= 'Fizz'; if ($n % 5 === 0) $s .= 'Buzz';
$s = ($s === '') ? (string)$n : $s;
$fizzbuzz[$n] = function () use ($s) {
return $s;
};
}
クロージャの特徴
関数定義文ではなく関数式 使い捨て関数を簡単に作れる 関数式は で変数捕捉できる
use
Closureクラスのオブジェクト で、変数や引数として扱える
これで呼び出せるようになった
<?php
assert($fizzbuzz[1]() === "1"); assert($fizzbuzz[2]() === "2");
assert($fizzbuzz[3]() === "Fizz");
assert($fizzbuzz[4]() === "4");
assert($fizzbuzz[5]() === "Buzz");
クロージャを使った
コード
クロージャ+array_map
<?php
$x2 = function ($n){ return $n * 2; };
$a = range(1, 10);
var_dump(array_map($x2, $a));
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
クロージャをうまく使って
「関数を作る関数」も
作れる
クロージャを使った部分適用
<?php
$times = function ($n) {
return function ($m) use ($n) {
return $n * $m;
};
};
$a = range(1, 10);
echo json_encode(array_map($times(3), $a));
// [3,6,9,12,15,18,21,24,27,30]
マジックメソッド
オブジェクトの 挙動を制御する 特殊なメソッド
プロパティアクセス
を乗っ取ろう
アクセス不能プロパティを乗っ取る
<?php
class A {
private $data = [];
public function __get($name) {
$this->data[$name];
}
public function __set($name, $value) {
$this->data[$name] = $value;
}
}
__isset() と __unset() も定義すべき
アクセス不能
=privateなども含む
この仕様を悪用して こんなこともできる
外部から読み込みだけできる
<?php
trait PrivateRead {
public function __get($name) {
$this->$name;
}
}
/**
* @property-read int $id
* @property-read int $name
*/
class A {
use PrivateRead;
private $id;
private $name;
}
未定義プロパティに書き込み禁止
<?php
trait ReadOnly {
public function __set($name, $value) {
throw new OutOfRangeException();
}
}
class B {
use ReadOnly;
}
メソッドアクセスも
乗っ取ろう
オーバーロード
(ただしPHP用語)
出典: PHP: オーバーロード - Manual
https://www.php.net/manual/ja/language.oop5.overloading.php
Copyright © 2001-2019 The PHP Group
一般的なプログラミング 用語「オーバーロード」 とは全然別物なので注意
未定義のメソッド呼び出しを捕捉
<?php
class C {
public function __call($name, $args)
{
が呼ばれた
echo __CLASS__."::{$name}() \n";
}
}
Proxyパターンを簡単に実現
<?php
/**
* @mixin Foo
*/
class D {
private $foo;
public function (Foo $class) {
$this->foo = $class;
}
public function __call($name, $args)
{
return $this->foo->$name(...$args);
}
}
リフレクション
実行時に情報を取得
できるAPI
(一部上書き)
関数・クラス・オブジェクト・
メソッド・プロパティ・パラメー タ(引数)・返値の型・クラス定数
など
https://www.php.net/manual/ja/book.reflection.php
PHPUnitの
dataProvider
(パラメタライズテスト)
とも組み合わせられる
未定義のメソッド呼び出しを捕捉
<?php
class C {
public function __call($name, $args)
{
が呼ばれた
echo __CLASS__."::{$name}() \n";
}
}
まとめ
メタプログラミングの技術を
使うと
「コードに書いてある通りで
はない」動きをさせられる
代償としてPhpStorm などの支援を受けにく
くなることがある
大いなる力には
大いなる責任が伴う