PHP / 변수 / 유동 변수(변수 안에 변수) 만드는 방법

PHP에서는 $$name 또는 ${$name}으로 변수에 들어 있는 문자열을 다른 변수의 이름으로 사용할 수 있습니다. 공식 문서의 이름은 가변 변수(Variable variables)이며, 이 글 제목의 유동 변수도 같은 기능을 가리킵니다. 관련 값을 여러 개 관리하려는 목적이라면 번호가 붙은 변수를 늘리기보다 배열이 더 명확한 경우가 많습니다.

변수 값이 다른 변수의 이름이 됩니다

다음 코드를 basic.php로 UTF-8 저장하고 php basic.php로 실행합니다. PHP의 가변 변수 문서에서 $$와 중괄호 표현을 확인할 수 있습니다. 변수 이름을 담은 문자열에는 $ 기호를 붙이지 않습니다.

<?php
$name = 'greeting';
$$name = '안녕하세요';
echo $name, "\n";
echo $greeting, "\n";
echo ${$name}, "\n";
echo "선택한 값: {$$name}\n";

출력은 greeting, 안녕하세요, 안녕하세요, 선택한 값: 안녕하세요의 네 줄입니다. $name에는 이름 문자열 greeting이 그대로 있고 $$name에 값을 넣으면 $greeting이 만들어집니다. 변수가 다른 변수의 내부에 저장되는 자료구조가 아니라, 현재 범위에서 사용할 변수 이름을 동적으로 선택하는 기능입니다. 이미 같은 이름의 변수가 있으면 그 값을 바꿀 수 있으므로 충돌을 주의해야 합니다.

문자열 안의 복잡한 보간은 위처럼 {$$name}을 사용하거나 문자열 연결로 표현합니다. 문자열 안에서 쓰던 오래된 ${name} 형태의 보간과 코드에서 사용하는 ${$name}을 혼동하지 마세요. 여기의 예제는 문자열 바깥의 가변 변수 구문과 명시적 중괄호 보간을 사용합니다. 이름의 대소문자는 구별되며, 생성할 이름은 일반 변수 이름 규칙에 맞춰 일관되게 정하는 편이 읽기 쉽습니다.

반복문으로 번호가 붙은 변수 만들기

다음 numbered.php는 기존 배열의 값을 item_0·item_1·item_2라는 변수로 복사합니다. 배열 키를 이름에 그대로 붙이는 예제이므로 여기서는 0부터 이어지는 리스트를 사용합니다. 다른 키가 있는 배열에 적용하면 그 키에 맞는 이름이 만들어집니다.

<?php
$items = ['A', 'B', 'C'];
foreach ($items as $index => $value) {
    ${'item_' . $index} = $value;
}
echo $item_0, "\n";
echo $item_1, "\n";
echo $item_2, "\n";

php numbered.php로 실행하면 A·B·C가 한 줄씩 나옵니다. ${...} 안의 식이 item_0 같은 이름 문자열을 만든 다음 값을 대입합니다. 목록의 길이가 달라지면 고정된 $item_2가 없을 수도 있으므로, 실제 데이터가 가변 길이라면 원래 $items를 foreach로 읽는 것이 단순합니다. 정의하지 않은 변수를 읽어 경고를 내기보다 필요한 존재 조건을 먼저 확인합니다.

배열 인덱스와 함수 범위 구분

배열의 한 값을 변수 이름으로 쓸 때와, 이름으로 선택한 배열의 한 항목을 읽을 때는 중괄호 위치가 다릅니다. 아래 braces.php는 두 경우를 따로 실행하는 전체 예제입니다.

<?php
$names = ['alpha', 'beta'];
$beta = '선택한 변수';
echo ${$names[1]}, "\n";

$name = 'letters';
$letters = ['A', 'B', 'C'];
echo ${$name}[1], "\n";

첫 결과는 선택한 변수입니다. $names[1]의 값 beta를 이름으로 해석해 $beta를 읽습니다. 둘째는 B입니다. $name으로 $letters 배열을 선택한 다음 인덱스 1을 읽습니다. $$names[1]처럼 경계가 불명확한 표기 대신 ${$names[1]}와 ${$name}[1]로 의도를 명시하세요.

<?php
function localVariableDemo(): int
{
    $name = 'result';
    ${$name} = 42;
    return $result;
}
echo localVariableDemo(), "\n";
var_dump(isset($result));

함수 범위 예제 scope.php의 출력은 42와 bool(false)입니다. 함수 안에서 만든 $result가 자동으로 함수 밖에 생기지는 않습니다. 가변 변수도 해당 범위의 규칙을 따르므로 함수 밖의 값을 쓸 때는 명시적인 매개변수·반환값을 우선합니다. $this나 슈퍼글로벌을 동적으로 다루는 데에는 별도 제한도 있으므로 가변 변수로 모든 PHP 변수를 자유롭게 참조할 수 있다고 생각하면 안 됩니다.

외부 입력에는 허용된 배열 키를 사용

$_GET·$_POST의 키를 순회하면서 ${$key}에 그대로 대입하면 설정·권한 판단에 쓰는 기존 변수 이름과 충돌할 수 있습니다. 이름만 영문인지 검사하는 것으로 업무상 허용된 입력이 되는 것도 아닙니다. 입력을 별도 배열에 보관하고 허용한 키와 값의 범위를 함께 검사하는 편이 명확합니다.

<?php
$input = ['theme' => 'dark', 'language' => 'ko', 'is_admin' => '1'];
$preferences = ['theme' => 'light', 'language' => 'en'];
$allowed = [
    'theme' => ['light', 'dark'],
    'language' => ['ko', 'en'],
];
foreach ($allowed as $key => $validValues) {
    if (array_key_exists($key, $input) && is_string($input[$key])
        && in_array($input[$key], $validValues, true)) {
        $preferences[$key] = $input[$key];
    }
}
echo json_encode($preferences, JSON_UNESCAPED_UNICODE), "\n";

preferences.php의 출력은 {"theme":"dark","language":"ko"}입니다. 허용 목록에 없는 is_admin은 복사하지 않으며, theme을 배열이나 허용하지 않은 문자열로 바꾸면 기본값 light를 유지합니다. 실제 폼을 받는 코드에 적용할 때도 같은 타입·허용 값 검사를 유지합니다. array_key_exists()는 값이 NULL이어도 키 존재 여부를 확인하지만 이 예제에서는 뒤의 문자열 검사로 NULL을 제외합니다.

배열은 항목 개수와 키를 열거하고 검사하기 쉬우며 기존 지역 변수를 덮어쓰지 않고 한 자료구조에 모을 수 있습니다. 가변 변수는 기존 코드가 특정 변수 이름을 요구하는 등 필요가 분명한 좁은 범위에서 사용하고, 같은 종류의 데이터 목록이라면 배열 접근 $items[$index] 또는 연관 배열의 키 접근부터 검토하세요. 객체 속성을 동적으로 선택하는 문법은 별개의 범위이며 이 예제가 임의 객체 속성 생성을 권하는 것은 아닙니다.