Variable variables ($$var) — when and why (and why not)
Concept
Variable variables allow you to use the value of a variable as the name of another variable. The syntax $$var means "use the current value of $var as a variable name." So if $key = 'name' and $name = 'Alice', then $$key evaluates to $name, which is 'Alice'. This is sometimes called "double dollar" or "dynamic variables."
Variable variables were more commonly used in PHP 4 and PHP 5 codebases before associative arrays and objects became the idiomatic way to handle dynamic data. The main legitimate historical use cases were: template engines that imported an array of variables into the local scope (extract()), dynamic form processing where field names drove variable names, and plugin systems that needed to call functions by string name.
In modern PHP 8+, variable variables are almost always a code smell. The problems are severe: (1) static analysis tools (Psalm, PHPStan) cannot type-check $$var because the variable name is unknown at analysis time, giving you no IDE autocomplete and no type errors; (2) they make code nearly impossible to refactor safely; (3) they are a well-known attack vector — if user input flows into $$userInput, an attacker can overwrite arbitrary variables in scope; (4) they complicate OPcache optimization because the runtime cannot statically determine which variables are defined.
The canonical modern replacements: use associative arrays ($data[$key]) for dynamic key-value access, use $object->$property (variable property access) or reflection for dynamic property access, and use compact() and extract() sparingly and only with a known-safe whitelist.
One legitimate advanced use case remains: in framework internals where a class builds a dynamically named method call. Even there, it is typically cleaner to use call_user_func([$obj, $methodName], ...$args) or the Reflection API instead of $$varName.
Code Example
<?php
declare(strict_types=1);
// Basic variable variable
$property = 'color';
$$property = 'blue'; // creates $color = 'blue'
echo $color; // 'blue'
echo $$property; // 'blue'
// Nested variable variables (three dollars)
$a = 'b';
$b = 'c';
$c = 'hello';
echo $$$a; // follows: $a='b', $b='c', $c='hello' -> 'hello'
// Historical use: loop over dynamic variables
$fields = ['title', 'body', 'author'];
foreach ($fields as $field) {
$$field = $_POST[$field] ?? ''; // creates $title, $body, $author
}
// Problem: static analysis has no idea what variables exist
// The modern replacement: associative array
$data = [];
foreach ($fields as $field) {
$data[$field] = $_POST[$field] ?? '';
}
echo $data['title']; // type-safe, analysable
// Variable property access (a milder form)
class Config
{
public string $host = 'localhost';
public int $port = 3306;
public string $name = 'mydb';
}
$config = new Config();
$prop = 'host';
echo $config->$prop; // 'localhost' — variable property access
// The reflection alternative (verbose but type-safe):
$ref = new ReflectionProperty(Config::class, $prop);
echo $ref->getValue($config); // 'localhost'
// Security hazard — NEVER do this with user input
function dangerousExtract(array $data): void
{
foreach ($data as $key => $value) {
$$key = $value; // if $key = 'password', attacker overwrites $password
}
}
// extract() with EXTR_PREFIX_ALL for safety
$safeData = ['name' => 'Alice', 'age' => 30];
extract($safeData, EXTR_PREFIX_ALL, 'row');
echo $row_name; // 'Alice'
echo $row_age; // 30
// compact() — the inverse of extract
$name = 'Alice';
$email = 'alice@example.com';
$user = compact('name', 'email');
// ['name' => 'Alice', 'email' => 'alice@example.com']
// When variable variables are used inside frameworks (still not recommended):
class TemplateEngine
{
private array $vars = [];
public function assign(string $name, mixed $value): void
{
$this->vars[$name] = $value; // store in array, NOT as $$name
}
public function render(string $template): string
{
extract($this->vars, EXTR_SKIP); // EXTR_SKIP: don't overwrite existing vars
ob_start();
include $template;
return ob_get_clean();
}
}Interview Q&A
Q: When are variable variables legitimate to use in modern PHP, and what are the security risks if user input reaches a $$var expression?
Legitimate use of variable variables in modern PHP is extremely narrow — primarily in code-generation tools, testing harnesses that dynamically set properties, or legacy template engines. Even in those cases, reflection or associative arrays are almost always preferable. The security risk is critical: if user input reaches $$input = $value, an attacker who controls $input can overwrite any variable in the current scope — including $_SERVER, local authentication flags, or intermediate computed values. For example, if your code previously set $isAdmin = false and an attacker submits input=isAdmin&value=1, the assignment $$input = $value overwrites $isAdmin with '1'. This is a form of variable injection, similar to the classic register_globals vulnerability that PHP 5.4 finally removed. Always validate $input against a whitelist of allowed variable names before using it as a variable variable.
Q: What is the difference between variable variables $$var, variable property access $obj->$prop, and call_user_func([$obj, $method])?
Variable variables ($$var) look up a named variable in the current scope — they operate on the symbol table. Variable property access ($obj->$prop) looks up a property on an object instance — it first checks declared properties, then falls through to __get() if defined. call_user_func([$obj, $method]) invokes a method by name and properly handles visibility (it throws Error if the method is private), returns the method's return value, and is understood by static analysis tools as a dynamic dispatch. In terms of safety and static analysis support: call_user_func is the cleanest (analyzers can at least track that it returns mixed), variable property access is acceptable for known property names, and variable variables are the most opaque and least safe.
Q: How does PHP's extract() function relate to variable variables, and what are the EXTR_* flags that make it safer?
extract() is essentially a loop that does $$key = $value for each entry in the provided array, importing those key-value pairs as variables into the current scope. Without flags, if an imported key matches an existing variable name it silently overwrites it — a major source of bugs and security issues. The EXTR_PREFIX_ALL flag prepends a prefix to every imported variable name (extract($data, EXTR_PREFIX_ALL, 'row') creates $row_name, $row_age, etc.), isolating them from existing variables. EXTR_SKIP preserves existing variables when there is a name collision. EXTR_OVERWRITE (the default) is the dangerous one. EXTR_PREFIX_INVALID only prefixes names that are not valid PHP identifiers. Best practice: never use extract() with $_POST, $_GET, or any untrusted array — always specify EXTR_PREFIX_ALL and a unique prefix, or better yet, use associative array access directly.