こんにちは!今回は、PHPのrunkit7拡張機能で提供されるrunkit7_method_add()関数について詳しく解説していきます。既存のクラスに実行時にメソッドを追加できる、非常に強力な関数です!
runkit7_method_add関数とは?
runkit7_method_add()関数は、既存のクラスに新しいメソッドを実行時に動的に追加することができる関数です。
通常、クラスのメソッドは定義時に決まりますが、この関数を使えば実行中にメソッドを追加して機能を拡張できます!
基本的な構文
runkit7_method_add(
string $classname,
string $methodname,
string $args,
string $code,
int $flags = RUNKIT7_ACC_PUBLIC,
string $doc_comment = null,
string $return_type = null,
bool $is_strict = null
): bool
- $classname: メソッドを追加するクラス名
- $methodname: 追加するメソッドの名前
- $args: 引数リスト(カンマ区切りの文字列)
- $code: メソッド本体のコード
- $flags: アクセス修飾子とその他のフラグ
RUNKIT7_ACC_PUBLIC(デフォルト)RUNKIT7_ACC_PROTECTEDRUNKIT7_ACC_PRIVATERUNKIT7_ACC_STATIC(静的メソッド)
- $doc_comment: ドキュメントコメント(オプション)
- $return_type: 戻り値の型(オプション)
- $is_strict: strict_types宣言(オプション)
- 戻り値: 成功時に
true、失敗時にfalse
前提条件
runkit7拡張機能のインストール
pecl install runkit7
php.iniに以下を追加:
extension=runkit7.so
runkit.internal_override=1
インストール確認:
<?php
if (function_exists('runkit7_method_add')) {
echo "runkit7が利用可能です!";
} else {
echo "runkit7がインストールされていません";
}
?>
基本的な使用例
シンプルなメソッドの追加
// 既存のクラス
class User {
private $name;
public function __construct($name) {
$this->name = $name;
}
public function getName() {
return $this->name;
}
}
$user = new User('田中太郎');
echo $user->getName() . "\n"; // 出力: 田中太郎
// 新しいメソッドを追加
runkit7_method_add(
'User',
'greet',
'',
'return "こんにちは、" . $this->name . "さん!";'
);
// 追加したメソッドを使用
echo $user->greet() . "\n"; // 出力: こんにちは、田中太郎さん!
引数を持つメソッドの追加
class Calculator {
private $value = 0;
public function getValue() {
return $this->value;
}
}
// 加算メソッドを追加
runkit7_method_add(
'Calculator',
'add',
'$number',
'$this->value += $number; return $this;'
);
// 乗算メソッドを追加
runkit7_method_add(
'Calculator',
'multiply',
'$number',
'$this->value *= $number; return $this;'
);
$calc = new Calculator();
$calc->add(10)->multiply(2)->add(5);
echo $calc->getValue() . "\n"; // 出力: 25
静的メソッドの追加
class MathHelper {
// 既存のメソッド
public static function square($n) {
return $n * $n;
}
}
echo MathHelper::square(5) . "\n"; // 出力: 25
// 静的メソッドを追加
runkit7_method_add(
'MathHelper',
'cube',
'$n',
'return $n * $n * $n;',
RUNKIT7_ACC_PUBLIC | RUNKIT7_ACC_STATIC
);
echo MathHelper::cube(5) . "\n"; // 出力: 125
実践的な使用例
例1: プラグインシステムの実装
class Application {
private $name;
public function __construct($name) {
$this->name = $name;
}
public function getName() {
return $this->name;
}
}
class PluginManager {
private $loadedPlugins = [];
public function loadPlugin($pluginName, $methods) {
echo "プラグイン '{$pluginName}' を読み込み中...\n";
foreach ($methods as $methodName => $methodInfo) {
list($args, $code, $flags) = array_pad($methodInfo, 3, RUNKIT7_ACC_PUBLIC);
runkit7_method_add('Application', $methodName, $args, $code, $flags);
if (!isset($this->loadedPlugins[$pluginName])) {
$this->loadedPlugins[$pluginName] = [];
}
$this->loadedPlugins[$pluginName][] = $methodName;
echo " メソッド {$methodName} を追加\n";
}
}
public function unloadPlugin($pluginName) {
if (!isset($this->loadedPlugins[$pluginName])) {
echo "プラグイン '{$pluginName}' は読み込まれていません\n";
return false;
}
echo "プラグイン '{$pluginName}' をアンロード中...\n";
foreach ($this->loadedPlugins[$pluginName] as $methodName) {
if (method_exists('Application', $methodName)) {
runkit7_method_remove('Application', $methodName);
echo " メソッド {$methodName} を削除\n";
}
}
unset($this->loadedPlugins[$pluginName]);
return true;
}
}
// 使用例
$app = new Application('MyApp');
$manager = new PluginManager();
// ロギングプラグインを読み込み
$manager->loadPlugin('LoggingPlugin', [
'log' => [
'$message',
'echo "[" . $this->name . "] " . $message . "\n";'
],
'logError' => [
'$error',
'echo "[" . $this->name . " ERROR] " . $error . "\n";'
]
]);
// キャッシングプラグインを読み込み
$manager->loadPlugin('CachingPlugin', [
'cache' => [
'$key, $value',
'static $cache = []; $cache[$key] = $value; return $value;'
],
'getCache' => [
'$key',
'static $cache = []; return $cache[$key] ?? null;'
]
]);
// プラグインのメソッドを使用
$app->log('アプリケーション起動');
$app->cache('user_count', 100);
echo "キャッシュ値: " . $app->getCache('user_count') . "\n";
// プラグインをアンロード
$manager->unloadPlugin('LoggingPlugin');
例2: 動的なバリデーションメソッドの追加
class FormValidator {
private $data = [];
private $errors = [];
public function __construct($data) {
$this->data = $data;
}
public function getData() {
return $this->data;
}
public function getErrors() {
return $this->errors;
}
public function hasErrors() {
return count($this->errors) > 0;
}
}
class ValidationRuleBuilder {
public static function addRule($ruleName, $fieldName, $errorMessage, $validationCode) {
$methodName = 'validate' . ucfirst($ruleName) . ucfirst($fieldName);
$code = '
$value = isset($this->data["' . $fieldName . '"]) ? $this->data["' . $fieldName . '"] : null;
$isValid = ' . $validationCode . ';
if (!$isValid) {
$this->errors["' . $fieldName . '"] = "' . $errorMessage . '";
}
return $this;
';
runkit7_method_add('FormValidator', $methodName, '', $code);
echo "バリデーションルール追加: {$methodName}\n";
}
}
// バリデーションルールを動的に追加
ValidationRuleBuilder::addRule(
'required',
'username',
'ユーザー名は必須です',
'!empty($value)'
);
ValidationRuleBuilder::addRule(
'email',
'email',
'有効なメールアドレスを入力してください',
'filter_var($value, FILTER_VALIDATE_EMAIL) !== false'
);
ValidationRuleBuilder::addRule(
'minLength',
'password',
'パスワードは8文字以上必要です',
'strlen($value) >= 8'
);
// 使用例
$data = [
'username' => '',
'email' => 'invalid-email',
'password' => 'short'
];
$validator = new FormValidator($data);
$validator->validateRequiredUsername()
->validateEmailEmail()
->validateMinLengthPassword();
if ($validator->hasErrors()) {
echo "バリデーションエラー:\n";
foreach ($validator->getErrors() as $field => $error) {
echo " {$field}: {$error}\n";
}
}
例3: デバッグメソッドの動的追加
class DataProcessor {
private $data = [];
public function addData($key, $value) {
$this->data[$key] = $value;
return $this;
}
public function getData() {
return $this->data;
}
}
class DebugHelper {
private static $enabled = false;
public static function enable() {
if (self::$enabled) {
echo "デバッグモードは既に有効です\n";
return;
}
echo "デバッグモードを有効化...\n";
// dumpメソッドを追加
runkit7_method_add(
'DataProcessor',
'dump',
'',
'
echo "=== データダンプ ===\n";
var_dump($this->data);
return $this;
'
);
// debugInfoメソッドを追加
runkit7_method_add(
'DataProcessor',
'debugInfo',
'',
'
echo "データ件数: " . count($this->data) . "\n";
echo "キー: " . implode(", ", array_keys($this->data)) . "\n";
return $this;
'
);
// traceメソッドを追加
runkit7_method_add(
'DataProcessor',
'trace',
'$message',
'
$trace = debug_backtrace();
$caller = isset($trace[1]) ? $trace[1]["function"] : "unknown";
echo "[TRACE from {$caller}] {$message}\n";
return $this;
'
);
self::$enabled = true;
echo "デバッグメソッドを追加しました\n";
}
public static function disable() {
if (!self::$enabled) {
echo "デバッグモードは既に無効です\n";
return;
}
echo "デバッグモードを無効化...\n";
runkit7_method_remove('DataProcessor', 'dump');
runkit7_method_remove('DataProcessor', 'debugInfo');
runkit7_method_remove('DataProcessor', 'trace');
self::$enabled = false;
echo "デバッグメソッドを削除しました\n";
}
}
// 使用例
$processor = new DataProcessor();
$processor->addData('name', '田中')
->addData('age', 30)
->addData('city', '東京');
// デバッグモードを有効化
DebugHelper::enable();
// デバッグメソッドが使用可能に
$processor->dump()
->debugInfo()
->trace('処理完了');
// デバッグモードを無効化
DebugHelper::disable();
例4: チェーンメソッドの動的生成
class QueryBuilder {
private $query = '';
public function getQuery() {
return trim($this->query);
}
}
class QueryMethodGenerator {
public static function addSelectMethod() {
runkit7_method_add(
'QueryBuilder',
'select',
'$columns = "*"',
'
$this->query .= "SELECT {$columns} ";
return $this;
'
);
}
public static function addFromMethod() {
runkit7_method_add(
'QueryBuilder',
'from',
'$table',
'
$this->query .= "FROM {$table} ";
return $this;
'
);
}
public static function addWhereMethod() {
runkit7_method_add(
'QueryBuilder',
'where',
'$condition',
'
$this->query .= "WHERE {$condition} ";
return $this;
'
);
}
public static function addOrderByMethod() {
runkit7_method_add(
'QueryBuilder',
'orderBy',
'$column, $direction = "ASC"',
'
$this->query .= "ORDER BY {$column} {$direction} ";
return $this;
'
);
}
public static function addLimitMethod() {
runkit7_method_add(
'QueryBuilder',
'limit',
'$limit',
'
$this->query .= "LIMIT {$limit} ";
return $this;
'
);
}
public static function buildAllMethods() {
self::addSelectMethod();
self::addFromMethod();
self::addWhereMethod();
self::addOrderByMethod();
self::addLimitMethod();
echo "すべてのクエリメソッドを追加しました\n";
}
}
// メソッドを生成
QueryMethodGenerator::buildAllMethods();
// 使用例
$qb = new QueryBuilder();
$query = $qb->select('id, name, email')
->from('users')
->where('age > 18')
->orderBy('name', 'DESC')
->limit(10)
->getQuery();
echo $query . "\n";
// 出力: SELECT id, name, email FROM users WHERE age > 18 ORDER BY name DESC LIMIT 10
例5: 機能フラグによるメソッドの有効化
class Product {
private $name;
private $price;
public function __construct($name, $price) {
$this->name = $name;
$this->price = $price;
}
public function getName() {
return $this->name;
}
public function getPrice() {
return $this->price;
}
}
class FeatureToggle {
private static $features = [];
public static function enableFeature($featureName) {
if (isset(self::$features[$featureName]) && self::$features[$featureName]) {
echo "機能 '{$featureName}' は既に有効です\n";
return;
}
echo "機能 '{$featureName}' を有効化...\n";
switch ($featureName) {
case 'discount':
self::enableDiscountFeature();
break;
case 'tax':
self::enableTaxFeature();
break;
case 'coupon':
self::enableCouponFeature();
break;
default:
echo "不明な機能: {$featureName}\n";
return;
}
self::$features[$featureName] = true;
}
private static function enableDiscountFeature() {
runkit7_method_add(
'Product',
'applyDiscount',
'$percentage',
'
$discount = $this->price * ($percentage / 100);
$this->price -= $discount;
return $this;
'
);
runkit7_method_add(
'Product',
'getDiscountedPrice',
'$percentage',
'
return $this->price * (1 - $percentage / 100);
'
);
echo " 割引機能のメソッドを追加\n";
}
private static function enableTaxFeature() {
runkit7_method_add(
'Product',
'getPriceWithTax',
'$taxRate = 0.1',
'
return $this->price * (1 + $taxRate);
'
);
echo " 税金計算機能のメソッドを追加\n";
}
private static function enableCouponFeature() {
runkit7_method_add(
'Product',
'applyCoupon',
'$couponValue',
'
$this->price -= $couponValue;
if ($this->price < 0) $this->price = 0;
return $this;
'
);
echo " クーポン機能のメソッドを追加\n";
}
public static function disableFeature($featureName) {
if (!isset(self::$features[$featureName]) || !self::$features[$featureName]) {
echo "機能 '{$featureName}' は有効になっていません\n";
return;
}
echo "機能 '{$featureName}' を無効化...\n";
switch ($featureName) {
case 'discount':
runkit7_method_remove('Product', 'applyDiscount');
runkit7_method_remove('Product', 'getDiscountedPrice');
break;
case 'tax':
runkit7_method_remove('Product', 'getPriceWithTax');
break;
case 'coupon':
runkit7_method_remove('Product', 'applyCoupon');
break;
}
self::$features[$featureName] = false;
}
}
// 使用例
$product = new Product('ノートパソコン', 100000);
echo "商品: {$product->getName()}\n";
echo "価格: {$product->getPrice()}円\n";
// 割引機能を有効化
FeatureToggle::enableFeature('discount');
echo "20%割引後: " . $product->getDiscountedPrice(20) . "円\n";
// 税金計算機能を有効化
FeatureToggle::enableFeature('tax');
echo "税込価格: " . $product->getPriceWithTax(0.1) . "円\n";
// クーポン機能を有効化
FeatureToggle::enableFeature('coupon');
$product->applyCoupon(5000);
echo "クーポン適用後: {$product->getPrice()}円\n";
例6: イベントハンドラーの動的追加
class EventEmitter {
private $listeners = [];
public function on($event, $callback) {
if (!isset($this->listeners[$event])) {
$this->listeners[$event] = [];
}
$this->listeners[$event][] = $callback;
return $this;
}
public function emit($event, $data = null) {
if (!isset($this->listeners[$event])) {
return;
}
foreach ($this->listeners[$event] as $callback) {
$callback($data);
}
}
}
class EventMethodGenerator {
public static function addEventMethod($className, $eventName) {
$methodName = 'on' . ucfirst($eventName);
runkit7_method_add(
$className,
$methodName,
'$callback',
'
return $this->on("' . $eventName . '", $callback);
'
);
$emitMethodName = 'emit' . ucfirst($eventName);
runkit7_method_add(
$className,
$emitMethodName,
'$data = null',
'
$this->emit("' . $eventName . '", $data);
return $this;
'
);
echo "イベント '{$eventName}' のメソッドを追加: {$methodName}, {$emitMethodName}\n";
}
}
// イベントメソッドを生成
EventMethodGenerator::addEventMethod('EventEmitter', 'userLogin');
EventMethodGenerator::addEventMethod('EventEmitter', 'userLogout');
EventMethodGenerator::addEventMethod('EventEmitter', 'dataUpdate');
// 使用例
$emitter = new EventEmitter();
$emitter->onUserLogin(function($user) {
echo "ユーザーログイン: {$user}\n";
});
$emitter->onUserLogout(function($user) {
echo "ユーザーログアウト: {$user}\n";
});
$emitter->onDataUpdate(function($data) {
echo "データ更新: {$data}\n";
});
// イベントを発火
$emitter->emitUserLogin('田中太郎');
$emitter->emitDataUpdate('商品情報');
$emitter->emitUserLogout('田中太郎');
例7: テストヘルパーメソッドの追加
class User {
private $id;
private $name;
private $email;
public function __construct($id, $name, $email) {
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
public function getId() {
return $this->id;
}
}
class TestHelper {
public static function addTestMethods($className) {
echo "テストヘルパーメソッドを {$className} に追加中...\n";
// アサーションメソッドを追加
runkit7_method_add(
$className,
'assertEquals',
'$expected, $actual, $message = ""',
'
if ($expected !== $actual) {
throw new Exception("Assertion failed: {$message}. Expected: {$expected}, Got: {$actual}");
}
echo "✓ アサーション成功\n";
return $this;
'
);
// モックデータ生成メソッドを追加
runkit7_method_add(
$className,
'toMockData',
'',
'
return [
"id" => $this->id,
"name" => "[MOCK] " . $this->name,
"email" => "[MOCK] " . $this->email
];
'
);
// デバッグ出力メソッドを追加
runkit7_method_add(
$className,
'debugPrint',
'',
'
echo "=== Debug Info ===\n";
echo "ID: {$this->id}\n";
echo "Name: {$this->name}\n";
echo "Email: {$this->email}\n";
return $this;
'
);
echo "テストヘルパーメソッドの追加完了\n";
}
public static function removeTestMethods($className) {
echo "テストヘルパーメソッドを削除中...\n";
runkit7_method_remove($className, 'assertEquals');
runkit7_method_remove($className, 'toMockData');
runkit7_method_remove($className, 'debugPrint');
echo "テストヘルパーメソッドの削除完了\n";
}
}
// テストメソッドを追加
TestHelper::addTestMethods('User');
$user = new User(1, '田中太郎', 'tanaka@example.com');
// テストメソッドを使用
$user->debugPrint();
$user->assertEquals(1, $user->getId(), 'ユーザーIDの確認');
$mockData = $user->toMockData();
print_r($mockData);
// テスト完了後、ヘルパーメソッドを削除
TestHelper::removeTestMethods('User');
アクセス修飾子の使用
public, protected, privateメソッド
class SecureData {
private $secret = '機密情報';
public function getPublicInfo() {
return 'これは公開情報です';
}
}
// publicメソッドを追加
runkit7_method_add(
'SecureData',
'publicMethod',
'',
'return "これは誰でもアクセスできます";',
RUNKIT7_ACC_PUBLIC
);
// protectedメソッドを追加
runkit7_method_add(
'SecureData',
'protectedMethod',
'',
'return "これは継承クラスからアクセスできます";',
RUNKIT7_ACC_PROTECTED
);
// privateメソッドを追加
runkit7_method_add(
'SecureData',
'privateMethod',
'',
'return $this->secret;',
RUNKIT7_ACC_PRIVATE
);
// publicヘルパーメソッドからprivateメソッドを呼び出し
runkit7_method_add(
'SecureData',
'accessPrivate',
'',
'return $this->privateMethod();'
);
$data = new SecureData();
echo $data->publicMethod() . "\n"; // OK
// echo $data->protectedMethod() . "\n"; // エラー
// echo $data->privateMethod() . "\n"; // エラー
echo $data->accessPrivate() . "\n"; // OK: 内部から呼び出し
静的メソッドと通常メソッドの組み合わせ
class Logger {
private static $logs = [];
private $instanceId;
public function __construct() {
$this->instanceId = uniqid();
}
}
// 静的メソッドを追加
runkit7_method_add(
'Logger',
'addLog',
'$message',
'
self::$logs[] = [
"timestamp" => date("Y-m-d H:i:s"),
"message" => $message
];
',
RUNKIT7_ACC_PUBLIC | RUNKIT7_ACC_STATIC
);
// インスタンスメソッドを追加
runkit7_method_add(
'Logger',
'log',
'$message',
'
self::addLog("[{$this->instanceId}] {$message}");
return $this;
'
);
// 静的メソッドで全ログを取得
runkit7_method_add(
'Logger',
'getAllLogs',
'',
'return self::$logs;',
RUNKIT7_ACC_PUBLIC | RUNKIT7_ACC_STATIC
);
// 使用例
$logger1 = new Logger();
$logger2 = new Logger();
$logger1->log('ログ1');
$logger2->log('ログ2');
Logger::addLog('直接追加されたログ');
print_r(Logger::getAllLogs());
重要な注意点と制限事項
1. 既存のメソッドは追加できない
class MyClass {
public function existingMethod() {
return "existing";
}
}
// 既存のメソッド名では失敗
$result = runkit7_method_add(
'MyClass',
'existingMethod',
'',
'return "new";'
);
var_dump($result); // bool(false)
$obj = new MyClass();
echo $obj->existingMethod(); // 出力: existing (変更されない)
2. 存在しないクラスには追加できない
// 存在しないクラスへの追加は失敗
$result = runkit7_method_add(
'NonExistentClass',
'newMethod',
'',
'return "test";'
);
var_dump($result); // bool(false)
3. 既存インスタンスへの影響
class TestClass {
private $value = 'original';
public function getValue() {
return $this->value;
}
}
// メソッド追加前にインスタンスを作成
$obj = new TestClass();
// 新しいメソッドを追加
runkit7_method_add(
'TestClass',
'setValue',
'$value',
'$this->value = $value;'
);
// 既存インスタンスでも新しいメソッドが使える
$obj->setValue('modified');
echo $obj->getValue() . "\n"; // 出力: modified
エラーハンドリングとベストプラクティス
安全なメソッド追加
function safeAddMethod($className, $methodName, $args, $code, $flags = RUNKIT7_ACC_PUBLIC) {
// クラスの存在確認
if (!class_exists($className)) {
echo "エラー: クラス {$className} が存在しません\n";
return false;
}
// メソッドが既に存在しないか確認
if (method_exists($className, $methodName)) {
echo "エラー: メソッド {$methodName} は既に存在します\n";
return false;
}
// メソッドを追加
$result = runkit7_method_add($className, $methodName, $args, $code, $flags);
if ($result) {
$visibility = ($flags & RUNKIT7_ACC_PRIVATE) ? 'private' :
(($flags & RUNKIT7_ACC_PROTECTED) ? 'protected' : 'public');
$static = ($flags & RUNKIT7_ACC_STATIC) ? 'static ' : '';
echo "成功: {$visibility} {$static}{$methodName}() を {$className} に追加しました\n";
} else {
echo "エラー: メソッドの追加に失敗しました\n";
}
return $result;
}
// 使用例
class TestClass {}
safeAddMethod('TestClass', 'test1', '', 'return "test1";');
safeAddMethod('TestClass', 'test1', '', 'return "test2";'); // エラー: 既に存在
safeAddMethod('NonExistent', 'test', '', 'return "test";'); // エラー: クラスが存在しない
まとめ
runkit7_method_add()関数の特徴をまとめると:
できること:
- 既存のクラスに実行時にメソッドを追加
- public/protected/privateの可視性指定
- 静的メソッドの追加
- プラグインシステムの実装
- 機能の動的拡張
注意点:
- runkit7拡張機能のインストールが必要
- 既存のメソッドは追加できない
- 存在しないクラスには追加できない
- 本番環境での使用は非推奨
推奨される使用場面:
- プラグインシステムの実装
- 開発・テスト環境でのデバッグ機能追加
- 機能フラグによる動的な機能追加
- レガシーコードの段階的拡張
より良い代替手段:
- トレイトの使用
- デコレータパターン
- 継承とオーバーライド
- コンポジションパターン
runkit7_method_add()は非常に強力ですが、コードの予測可能性を損なう可能性があります。通常のアプリケーション開発では、トレイトやデザインパターンを使った方が安全で保守性の高いコードになります!
