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

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をコピーしました