レガシープロジェクトでグローバル変数をリファクタするには

gotoの次にグローバル変数は嫌われているのかな

偶然にもどちらもG始まりね

PhpStormのリファクタ機能

まだ関数に分けていない状態なら、PhpStormのリファクタ機能を使うと便利です。引数を間違うことなく、関数に分けたり、クラスとプロパティにできますよ。

関数にする

関数に分けたい行を選択しておき、メニューのRefactor>Refactor this...>7. Extract method...でダイアログを表示します。

<?php $map = []; $a = 1; $map['foo'] = 'bar'; // ココと $a += 2; // ココを選択します echo $map; echo $a;
Code language: PHP (php)

表示されたダイアログで関数名を入力して、参照渡し/返り値を選択することで、リファクタしてくれます。

返り値を選択して、自動リファクタした例

<?php $map = []; $a = 1; /** * @param array $map * @param int $a * @return array */ function foo(array $map, int $a): array { $map['foo'] = 'bar'; $a += 2; return array($map, $a); } list($map, $a) = foo($map, $a); echo $map; echo $a;
Code language: PHP (php)

PhpStormすごい!

クラスやプロパティにする

クラスにしたり、$mapをプロパティにして、コンストラクタ渡しにしたり、さらにメソッドに分けたりできます。

元の状態です。

<?php $map = []; $a = 1; $map['foo'] = 'bar'; $a += 2; echo $map; echo $a;
Code language: PHP (php)

まず「<?php」を除く全行を選択して、Refactor>Refactor this…>7. Extract method…で、ダイアログを表示して、関数にします。

<?php function main(): void { $map = []; $a = 1; $map['foo'] = 'bar'; $a += 2; echo $map; echo $a; } main();
Code language: PHP (php)

main関数の内部を全て選択して、Refactor>Refactor this…>8. Extract class…で、ダイアログを表示して、クラスにします。

<?php Main::main();
Code language: PHP (php)
<?php class Main { public static function main(): void { $map = []; $a = 1; $map['foo'] = 'bar'; $a += 2; echo $map; echo $a; } }
Code language: PHP (php)

手作業で、mainメソッドのstaticを削除し、呼ぶ側も修正します。

<?php $main = new Main(); $main->main();
Code language: PHP (php)
public function main(): void {
Code language: PHP (php)

PhpStormのリファクタ機能を使って、mainメソッド内の$mapをインスタンスプロパティにします。

mainメソッド内の$mapをクリックし、Refactor>Refactor this…>6. Introduce Field…を選択します。

小さい選択ウィンドウが表示されて、$map、$map = [] のどちらをプロパティにするか聞かれます。$mapを選択します。

class Main { private $map; public function main(): void { $this->map = []; $a = 1; $this->map['foo'] = 'bar'; $a += 2; echo $this->map; echo $a; } }
Code language: PHP (php)

$mapが $this->map に変化していますね!

手作業で、空のコンストラクタを追加します。

class Main { private $map; public function __construct() { } public function main(): void {
Code language: PHP (php)

PhpStromのリファクタ機能で、コンストラクタの引数に$mapを追加します。

private $map;行の$mapを右クリックし、Show Context Actionsをクリックします。

class Main { private $map; public function __construct($map) { $this->map = $map; } public function main(): void { $this->map = []; $a = 1; $this->map['foo'] = 'bar'; $a += 2; echo $this->map; echo $a; } }
Code language: PHP (php)

次の2行を選択して、Refactor>Refactor this…>7. Extract method…で、fooメソッドにします。

$this->map['foo'] = 'bar'; $a += 2;
Code language: PHP (php)
<?php class Main { private $map; public function __construct($map) { $this->map = $map; } public function main(): void { $this->map = []; $a = 1; $a = $this->foo($a); echo $this->map; echo $a; } /** * @param int $a * @return int */ public function foo(int $a): int { $this->map['foo'] = 'bar'; $a += 2; return $a; } }
Code language: PHP (php)

$aが引数で渡されて、返り値で戻ってきています。すごいですね!

PhpStormべんり〜

引数で渡す

グローバル変数を使った関数の例です。

<?php function foo() { global $map; global $a; $map['foo'] = 'bar'; $a += 2; } $map = []; $a = 1; foo(); echo $map; echo $a;
Code language: PHP (php)

関数内で、グローバル変数の値を変更していたり、配列の要素を追加している場合は、引数を参照渡しする方法と、返り値で返す方法があります。

引数を参照渡し

引数を参照渡しする方法です。

<?php function foo(&$map, &$a) { $map['foo'] = 'bar'; $a += 2; } $map = []; $a = 1; foo($map, $a); echo $map; echo $a;
Code language: PHP (php)

引数を値渡し+返り値

引数は値渡しして、変更した結果を返り値で返す方法です。

<?php function foo($map, $a) { $map['foo'] = 'bar'; $a += 2; return [$map, $a]; } $map = []; $a = 1; list($map, $a) = foo($map, $a); echo $map; echo $a;
Code language: PHP (php)

コンストラクタの引数で渡して、privateプロパティでアクセスする

さきほどと同じ例ですが、関数は長い処理をしています。いずれクラスにして複数のメソッドに分けてリファクタしたい、そんな関数です。

<?php function foo() { global $map; global $a; $map['foo'] = 'bar'; $a += 2; // 長い処理 } $map = []; $a = 1; foo(); echo $map; echo $a;
Code language: PHP (php)

コンストラクタに参照渡し

まず、参照渡しバージョンです。

引数並びに & をつけ、privateプロパティに代入するときも & をつけます。

<?php class FooAction { private $map; private $a; public function __construct(&$map, &$a) // & で参照渡し{ $this->map = &$map; // & で参照渡し $this->a = &$a; // & で参照渡し } public function foo() { $this->map['foo'] = 'bar'; $this->a += 2; // 長い処理 } } $map = []; $a = 1; $fooAction = new FooAction($map, $a); $fooAction->foo(); echo $map; echo $a;
Code language: PHP (php)

コンストラクタに値渡し+resultメソッド

コンストラクタで値渡しする方法です。呼んだ側に値を戻すために、resultメソッドを用意した例です。

<?php class FooAction { private $map; private $a; public function __construct($map, $a) { $this->map = &$map; $this->a = &$a; } public function foo() { $this->map['foo'] = 'bar'; $this->a += 2; } public function results() { return [$this->map, $this->a]; } } $map = []; $a = 1; $fooAction = new FooAction($map, $a); $fooAction->foo(); list($map, $a) = $fooAction->results(); echo $map; echo $a;
Code language: PHP (php)

別クラスのメソッドにしたので、リファクタしやすくなりました。

コールバック関数

PHPは関数の内側と外側で変数スコープが分かれています。今なら、クロージャとuseを使うことが多いでしょう。PHP 5.2以前はクロージャがないので、コールバック関数を使うのが面倒な場面がありました。

1つめの問題は、array_walkのコールバック関数には、3つめの引数$userdataがありますが、array_mapやusortのコールバック関数には$userdataがないことです。そのため、array_mapやusortを呼んでいる側の変数をコールバック関数は参照できません。そのため、global宣言していることがあります。

2つめの問題は、array_walkの$userdataを使うとき、コールバック関数で$userdataの値を変更しても、呼んだ側の$userdataに値が反映されないことです。そのため、global宣言していることがあります。

for文、foreach文にもどす

$arrの各要素に$foo->calc()を計算して、新しい配列を$new_arrを作る例です。

<?php class Foo { public function calc($x, $a) { return $x + $a; } } global $foo; $foo = new Foo(); $a = 2; $arr = [1, 2, 3, 4]; $new_arr = array_map('my_callback', $arr); // $new_arr = [3, 4, 5, 6]; function my_callback($val) { global $foo; global $a; return $foo->calc($val, $a); }
Code language: PHP (php)

コールバック関数が1行なら、foreach文に戻してもいいでしょう。

foreach文にしました。$fooや$aをglobal宣言する必要がありません。

$new_arr = []; foreach ($arr as $val) { $new_arr[] = $foo->calc($val, $a); }
Code language: PHP (php)

クロージャのuseを使う

もともとのコールバック関数はやめ、クロージャとuseを使った例です。useで、$fooと$aを渡します。

$new_arr = array_map(function ($val) use ($foo, $a) { return $foo->calc($val, $a); }, $arr);
Code language: PHP (php)

もともとのコールバック関数が数行以上ある場合は残しておきます。引数に $foo を追加して、クロージャから元のコールバック関数を呼びます。

$new_arr = array_map(function ($val) use ($foo, $a) { return my_callback($val, $foo, $a); }, $arr); function my_callback($val, $foo, $a) { // 何か処理 return $foo->calc($val, $a); }
Code language: PHP (php)

クラスのインスタンスプロパティを使う

メインロジックにクロージャやコールバック関数が並んでいると、メインロジックがゴチャゴチャした感じになります。

そこで、別クラスにまとめて、グローバル変数だった$fooをインスタンスプロパティで置き換えた例です。

$foo = new Foo(); $bar = new Bar($foo, $a); $new_arr = $bar->array_map($arr);
Code language: PHP (php)
<?php class Bar { private $foo; private $a; public function __construct($foo, $a) { $this->foo = $foo; $this->a = $a; } public function array_map($arr) { return array_map([$this,'my_callback'], $arr); } private function my_callback($val) { // 何か処理 return $this->foo->calc($val, $this->a); } }
Code language: PHP (php)

userdataと参照を使う

説明のため、array_walkで合計を計算します。(本来はarray_sumを使って、配列の合計を計算できます)

<?php $arr = [1, 2, 3, 4]; global $g_sum; $g_sum = 0; array_walk($arr, 'my_callback'); var_dump($g_sum); function my_callback($value, $index) { global $g_sum; $g_sum += $value; }
Code language: PHP (php)

まず、反映されない使い方です。

コールバック内で3つめの引数$userdataの値を読むだけなら、これで問題ありません。

しかし、値を変更したい場合、次の使い方では、変更が反映されず、結果は 0 と表示されます。

ところが、コールバック内で、$g_sumを表示すると、3, 6, 10と増えていきます。何が悪いのでしょうか?

<?php $arr = [1, 2, 3, 4]; $g_sum = 0; array_walk($arr, 'my_callback', $g_sum); var_dump($g_sum); function my_callback($value, $index, &$g_sum) { $g_sum += $value; var_dump($g_sum); }
Code language: PHP (php)

反映される使い方です。

$userdataを参照のコンテナとして使います。

<?php $arr = [1, 2, 3, 4]; $g_sum = 0; $userdata = [ 'sum' => &$g_sum, // & をつける ]; array_walk($arr, 'my_callback', $userdata); var_dump($g_sum); // $userdataに & をつけなくてもいい function my_callback($value, $index, $userdata) { $userdata['sum'] += $value; }
Code language: PHP (php)

IO処理のオブジェクトのメソッドに移す

話しがずれますが、一つのメソッドや関数で、
「データを加工」+「IO処理で保存」
または
「IO処理から読み込み」+「データを加工」
しているのは、よく見かけます。

何が気になるかというと、IO処理があるとユニットテストを作りにくいんですね。テストケースでIO処理のモックをセットアップしていると、何をテストしているのか、よくわからないコードになりがちです。

さて、グローバル変数がIO処理のオブジェクトの場合です。foo関数は「データを加工」と「IO処理で保存」をしています。

<?php $bar = new Bar(); foo("foo", 123); function bar($name, $num) { global $bar; $str = $name . ":" . $num; $bar->save($str); }
Code language: PHP (php)

foo関数は、グローバル変数の$barを引数で渡すようにリファクタしても、ユニットケースを作るのは相変わらず難しいです。

そこで、foo関数から「IO処理で保存」をなくしてしまいます。グローバル変数は必要なくなり、foo関数のユニットテストを書きやすくなりました。

<?php $bar = new Bar(); $str = foo("foo", 123); $bar->save($str); function foo($name, $num) { $str = $name . ":" . $num; return $str; }
Code language: PHP (php)

さらに、このfoo()、$bar->save()の処理が、定形処理なら、Barクラスのメソッドにするのもいいと思います。

<?php $bar = new Bar(); $bar->fooSave("foo", 123);
Code language: PHP (php)
<?php class Bar { public function fooSave($name, $num) { $str = self::foo($name, $num); $this->save($str); } public static function foo($name, $num) { $str = $name . ":" . $num; return $str; }
Code language: PHP (php)

コールスタックの深いところから、呼び元へ値を返す

レガシープロジェクトに限らず、コールスタックの深いところから、呼び元へ値を返したいことがあります。そのとき、グローバル変数の代わりに使ってしまいがちなのが、$_SESSIONです。

だめなの?思いついたときは、ぼくってやるじゃん!って思ったけど。

本来は、返り値で返しながら、バブルアップで上位まで返したいのですが、途中のメソッドを変更できないことも多く、返り値で返せません。

$_SESSIONは時間的な生存期間が長いので、グローバル変数より注意が必要です。

また、global宣言よりも$GLOBALSのほうがいいです(記事の最後で説明します)。

直接$GLOBALSを読み書きしていいですし、

// コールスタックの深いところで $GLOBALS['FooReturnValue'] = 'bar'; // 呼んだ側で $retVal = $GLOBALS['FooReturnValue'] ?: '';
Code language: PHP (php)

$GLOBALSを読み書きするgetter/setterを作ってもいいと思います。

// コールスタックの深いところで Foo::setReturnValue('bar'); // 呼んだ側で $retVal = Foo::getReturnValue(); class Foo { public static function setReturnValue($value) { $GLOBALS['FooReturnValue'] = $value; } public static function getReturnValue() { return $GLOBALS['FooReturnValue'] ?: ''; } }
Code language: PHP (php)

実はグローバル変数

$_SESSION、staticプロパティ、関数のstatic変数は、実はグローバル変数です。globalや$GLOBALSの代わりに使うのはNGです。

putenv/getenv もグローバル変数として使うことができますが、もちろんグローバル変数として使うのはNGです。

遠く離れたオブジェクトに値を送るには、返り値で返すか、そのためのオブジェクトを引数で渡すしかありません。何のつながりもないオブジェクト間で、第三のオブジェクトを経由して値を交換できるとしたら、第三のオブジェクト自体がSingletonだったり、裏でstatic変数やグローバル変数的なものを保持しているはずです。

第三のオブジェクトを用意して、依存関係やセットアップコードを複雑にするよりは、$GLOBALSを使うほうがいいように思います。

$_SESSION

コールスタックの深いところから上位へ値を返すとき、使われがちなのが、$_SESSIONです。

同じスーパーグローバル変数でも、$_GET、$_POST、$_SERVERなどは、PHPが設定し、アプリケーションは読み出すだけです。通常は、値を読み出すだけであり、値が変わることはないので、グローバルでも安全です。

$GLOBALSや$_SESSIONは、いつでもどこからでも値を変更可能で、実際そのように使います。

さらに、$_SESSION変数の生存期間は、アプリケーションの実行期間より長いので、 global変数や$GLOBALSを使うより、注意が必要です。

数秒後か数分後の次のページアクセスのための変数を保存していいなら、コンマ何秒後のために保存してもいいと思うんだけど?

そのデータは、本当にコンマ何秒か前に保存されたデータなのかしら?もっと古いデータかもね?

staticプロパティ

staticプロパティは、global宣言しなくても使えるよ!

たしかに、グローバル変数をstaticプロパティに置き換えることができます。

<?php class MyVars { public static $map; public static $a; } function foo() { MyVars::$map['foo'] = 'bar'; MyVars::$a += 2; } MyVars::$map = []; MyVars::$a = 1; foo(); echo MyVars::$map; echo MyVars::$a;
Code language: PHP (php)

なにが良くないの?

global宣言は削除できましたが、グローバル変数を使っていることに変わりありません。

次のコードは何の問題もないように見えますが、

MyVars::$map = []; MyVars::$a = 1; foo(); doSomething();
Code language: PHP (php)

doSomething()内で、MyVars::$mapを書き換えてしまっています。

function doSomething() { MyVars::$map = []; }
Code language: PHP (php)

public staticプロパティは、グローバル変数と同じです。通常は、使わないほうが無難です。

関数のstatic変数

関数はglobal宣言なしで使えるよ?

もともとは、static変数にリテラル配列を定義していた例です。

<?php function myVars($num) { static $map = [ 1 => 'foo', 2 => 'bar', ]; return $map[$num]; }
Code language: PHP (php)
<?php echo myVars(1); echo myVars(2);
Code language: PHP (php)

ユーザーが編集したくなったのか、コードに書くには配列が大きくなってしまったのか、配列データをDBから読み込むようにしました。

しかし、static変数に設定する方法がわからなかったのか、リファクタが面倒だったのか、global宣言を使ってしまったようです。

<?php function myVars($num) { global $g_map; return $g_map[$num]; }
Code language: PHP (php)
<?php global $g_map; $g_map = load_from_db(); echo myVars(1); echo myVars(2);
Code language: PHP (php)

まず、関数のstatic変数に外部から設定する方法です。

<?php function myVars($num) { static $map = []; if (is_array($num)) { $map = $num; } else { return $map[$num]; } }
Code language: PHP (php)

呼ぶ側のコードです。

<?php $g_map = load_from_db(); myVars($g_map); echo myVars(1); echo myVars(2);
Code language: PHP (php)

ところが、このケースの関数のstatic変数は、グローバル変数と同じです。

次のコードは問題ないように見えますが、

<?php $g_map = load_from_db(); myVars($g_map); doSomething(); echo myVars(1); echo myVars(2);
Code language: PHP (php)

doSomething関数内で、myVars関数を呼びぶことができ、値を書き換えることができます。

function doSomething() { myVars([]); }
Code language: PHP (php)

このケースの場合は、ふつうのクラスのインスタンスプロパティにしたほうがいいです。

<?php class MyVars { private $map; public function __construct($map) { $this->map = $map; } public function myVars($num) { return $this->map[$num]; } }
Code language: PHP (php)

呼ぶ側のコードです。

<?php $g_map = load_from_db(); $myVars = new MyVars($g_map); foo(); echo $myVars->myVars(1); echo $myVars->myVars(2);
Code language: PHP (php)

使うならglobal宣言より$GLOBALS

やむをえずグローバル変数を使う場合は、global宣言よりも、$GLOBALSのほうがベターです。

$GLOBALSをvar_dumpしたり、デバッガで見ることで、ある時点のグローバル変数の利用状況を調べるのが簡単です。

global宣言は、ふつうのコード内の変数をグローバル化してしまいます。グローバル変数を表す変数名プリフィックス $g_をつけたとしても、その変数が「グローバル変数です、要注意!」と言っているようには見えません。

それに比べて、$GLOBALSがコード内にあると、大文字で目立ちます。周囲の変数と見た目が違うので「グローバル変数です、要注意!」と言っています。あやしいことをしているので、目立ったほうがいいです。

もうひとつ、global宣言のよくないところは、グローバル領域(関数やクラスの外側)の変数は、関数側でglobal宣言するだけで見えてしまうことです。

<?php // page1.php // 関数やクラスの外側 $x = 1; // 他の関数からglobal宣言で見えてしまうことを防げない bar(); // bar.php function bar() { global $x;// グローバル領域の$xが見える $x += 2; }
Code language: PHP (php)

逆に、foo関数とbar関数だけで共有したいグローバル変数なのに、グローバル領域のコードが知らずに上書きしてしまうかもしれません。

<?php // page1.php // 関数やクラスの外側 foo(); $x = 1; // foo関数とbar関数のグローバル共有に気づかずに上書き bar(); // foo.php function foo() { global $x; // bar関数と共有 $x += 2; } // bar.php function bar() { global $x; // foo関数と共有 $x += 3; }
Code language: PHP (php)

$GLOBALSは、グローバル領域、関数側のどちらも$GLOBALS['x']のように書く必要があります。グローバル変数と知らずに上書きすることはありません。

<?php // page1.php // 関数やクラスの外側 $GLOBALS['x'] = 1; // グローバル変数であることが一目瞭然 $x = 2; bar(); // bar.php function bar() { $GLOBALS['x'] = 2; // $x = 3; }
Code language: PHP (php)

いろいろ説明しているけど、global宣言も$GLOBALSも50歩100歩ね、使わないにことしたことはないわ

グローバル領域に変数を書かない

グローバル領域に変数を書かないようにします。main関数を書き、main関数を呼ぶようにします。main関数側が知らないうちに、他の関数からglobalで読まれたり、値を変更されたりすることを防げます。

<?php // page1.php function main() { $x = 1; // 他の関数でglobal宣言しても見えない foo(); $GLOBALS['x'] += 3; } main(); // bar.php function bar() { global $x; // main関数の$xは見えない $x = 2; $GLOBALS['x'] = 3; }
Code language: PHP (php)

即時関数にしてもいいと思います。

<?php // page1.php (function () { $x = 1; // 他の関数でglobal宣言しても見えない foo(); $GLOBALS['x'] += 3; })(); // bar.php function bar() { global $x; // main関数の$xは見えない $x = 2; $GLOBALS['x'] = 3; }
Code language: PHP (php)

JavaScriptも即時関数をよく使うよね

タイトルとURLをコピーしました