Low level metaprogramming in PHP
Long before PHP had Reflection or attributes, it already offered a way to treat function names and variable names themselves as values at runtime. Variable functions and variable variables are the oldest form of metaprogramming in PHP, powerful and risky at the same time, and still the foundation of many dynamic dispatchers today.
Table of Contents
- 1. What variable functions and variable variables are
- 2. Variable functions in detail: $fn() and callables from strings
- 3. Variable variables: $$name and their rare legitimate uses
- 4. Dynamic method calls: $obj->$method() and array callables
- 5. Practical example: a dynamic dispatcher for CLI commands
- 6. Practical example: dynamic property access for generic DTOs
- 7. Static analysis limits: why PHPStan gives up here
- 8. First class callable syntax as a type safe alternative
- 9. Variable functions/variables compared to alternatives
- 10. Summary
- 11. FAQ
1. What variable functions and variable variables are
Variable functions and variable variables are two of the oldest metaprogramming mechanisms in PHP, available since the very first versions of the language, long before Reflection, attributes or closures existed. A variable function occurs when the name of the function to be called is itself held in a variable and the syntax $name() is used instead of a literal function name. A variable variable occurs when the name of a variable is itself formed from another variable, using the syntax $$name.
Both mechanisms allow program structure that normally is fixed at development time to be derived from data only at runtime instead. A function name coming from a configuration file or user input can be invoked directly, without writing a long match chain. Before modern alternatives such as closures and Reflection existed, this flexibility was often the only practical solution for generic, data driven dispatchers.
The price of this flexibility is traceability: neither a human reader nor a static analysis tool can determine from the source code alone which concrete function or variable is actually addressed at runtime, once the name is only assembled from a variable. This article covers both mechanisms in detail, their legitimate use cases, and the modern alternatives that should be preferred in most cases.
2. Variable functions in detail: $fn() and callables from strings
The syntax for a variable function is remarkably simple: if a variable holds a string matching the name of an existing function, $variable() calls exactly that function. The same principle also works with method names combined with an array in the form [$object, 'methodName'], which PHP internally treats as a valid callable type, regardless of whether it is invoked through call_user_func() or directly via $callable().
For safely using variable functions, it is important to check with is_callable() before the actual call, because PHP throws a fatal error for a non existent function name that is difficult to catch in a controlled way. For calls whose names come from external, untrusted sources such as user input, an allowlist of known, permitted function names is additionally indispensable to prevent arbitrary code execution.
<?php
declare(strict_types=1);
function formatAsEuro(float $amount): string
{
return number_format($amount, 2, ',', '.') . ' EUR';
}
function formatAsPercentage(float $value): string
{
return number_format($value * 100, 1) . ' %';
}
// The function name comes from configuration, not a literal in the code
$formatterName = 'formatAsEuro';
// Always validate against an allowlist before invoking a dynamic name
$allowedFormatters = ['formatAsEuro', 'formatAsPercentage'];
if (in_array($formatterName, $allowedFormatters, true) && is_callable($formatterName)) {
echo $formatterName(1299.9) . PHP_EOL; // 1.299,90 EUR
}
3. Variable variables: $$name and their rare legitimate uses
A variable variable is created with the syntax $$name, where $name is itself a variable whose content is used as the name of another variable. If $name holds the string 'total', for example, $$name accesses the variable $total, regardless of whether it existed beforehand at all. This feature has existed since the earliest PHP versions and was originally used frequently to dynamically create variables in the current scope from HTTP form data.
That exact original use is now considered an anti pattern and is the main reason why register_globals was removed from PHP long ago: uncontrolled variable variables from user input could overwrite arbitrary, already existing variables in scope, leading to serious security vulnerabilities. Legitimate, rare uses of variable variables today are limited to internal, controlled contexts, for example programmatically setting several thematically related instance variables from a fixed, internal list of names, never from data coming directly from a user.
<?php
declare(strict_types=1);
final class QuarterlyReport
{
public float $q1 = 0.0;
public float $q2 = 0.0;
public float $q3 = 0.0;
public float $q4 = 0.0;
/** @param array<string, float> $values internal, trusted data only */
public function fillFromInternalData(array $values): void
{
// Controlled, internal use only — never with raw user input
foreach (['q1', 'q2', 'q3', 'q4'] as $quarterKey) {
if (isset($values[$quarterKey])) {
$this->$quarterKey = $values[$quarterKey];
}
}
}
}
$report = new QuarterlyReport();
$report->fillFromInternalData(['q1' => 1000.0, 'q2' => 1200.0]);
echo $report->q1 . PHP_EOL; // 1000
4. Dynamic method calls: $obj->$method() and array callables
Analogous to variable functions, PHP allows dynamic method calls with the syntax $object->$methodName(), where $methodName is a variable holding the name of the method to be called as a string. This technique is the foundation of many generic dispatchers that route an action based on a string, such as a command name or an event type, to the matching method without having to write an explicit match branch for every possible case.
Static method calls can likewise be made dynamic, with the syntax $class::$method() from PHP 8.0 onward, where both the class name and the method name can come from variables. The same safety note applies to all these variants as to variable functions: without checking with method_exists() and is_callable(), a non existent method name can lead to a fatal error that is hard to diagnose.
<?php
declare(strict_types=1);
final class OrderCommandHandler
{
public function create(array $payload): string
{
return 'Order created: ' . $payload['id'];
}
public function cancel(array $payload): string
{
return 'Order cancelled: ' . $payload['id'];
}
}
$handler = new OrderCommandHandler();
$command = 'create'; // e.g. resolved from a CLI argument or a message queue payload
if (method_exists($handler, $command) && is_callable([$handler, $command])) {
echo $handler->$command(['id' => 42]) . PHP_EOL; // Order created: 42
}
5. Practical example: a dynamic dispatcher for CLI commands
A classic use case for variable functions and dynamic method calls is a simple CLI dispatcher that accepts a command argument and calls the matching method on a handler, without writing a separate match branch for every possible command. This pattern significantly reduces boilerplate, especially when new commands are added regularly and the central dispatch logic is meant to stay unchanged.
The safety of this pattern depends entirely on the combination of allowlist and existence check. A dispatcher that passes command names unfiltered to a dynamic method potentially opens the door to invoking internal methods never meant for the CLI. The allowlist of known commands is therefore not an optional detail, but the central safety mechanism of this approach.
<?php
declare(strict_types=1);
final class CliDispatcher
{
/** @var string[] explicit allowlist of exposed command methods */
private const ALLOWED_COMMANDS = ['create', 'cancel'];
public function __construct(private readonly OrderCommandHandler $handler)
{
}
public function dispatch(string $command, array $payload): string
{
if (!in_array($command, self::ALLOWED_COMMANDS, true)) {
throw new InvalidArgumentException("Unknown command: {$command}");
}
if (!method_exists($this->handler, $command) || !is_callable([$this->handler, $command])) {
throw new RuntimeException("Command handler missing for: {$command}");
}
return $this->handler->$command($payload);
}
}
$dispatcher = new CliDispatcher(new OrderCommandHandler());
echo $dispatcher->dispatch('create', ['id' => 42]) . PHP_EOL; // Order created: 42
6. Practical example: dynamic property access for generic DTOs
Variable variables in the form of dynamic property access, meaning $object->$propertyName, are useful for generic data transfer objects that need to read or set values based on a field name known only at runtime, for example when mapping a row from a CSV file onto an object with fixed, typed properties. Unlike a full reflection based hydration, this approach stays limited to the simple case where all target properties are public.
The advantage over ReflectionProperty::setValue() lies in the lower overhead, since no reflection object needs to be constructed. The downside is that private or protected properties cannot be reached this way, and PHP silently creates a new, dynamic property on a misspelled property name unless the class is explicitly guarded against it, which can make typos invisible.
<?php
declare(strict_types=1);
final class ProductRow
{
public string $sku = '';
public float $price = 0.0;
public int $stock = 0;
}
/** @param array<string, string> $csvRow */
function mapCsvRowToProduct(array $csvRow): ProductRow
{
$product = new ProductRow();
// Only known, allowed property names are ever assigned dynamically
$allowedFields = ['sku', 'price', 'stock'];
foreach ($allowedFields as $field) {
if (!isset($csvRow[$field])) {
continue;
}
$product->$field = match ($field) {
'price' => (float) $csvRow[$field],
'stock' => (int) $csvRow[$field],
default => $csvRow[$field],
};
}
return $product;
}
$product = mapCsvRowToProduct(['sku' => 'SKU-1', 'price' => '19.99', 'stock' => '5']);
echo $product->price . PHP_EOL; // 19.99
7. Static analysis limits: why PHPStan gives up here
Tools like PHPStan and Psalm work by statically analyzing a program's control flow and types, meaning without executing it. With a call such as $this->$method(), where $method only receives a concrete value at runtime, no static tool can reliably determine which method is actually invoked, and consequently cannot check whether that method exists at all or whether the arguments passed match its signature.
This blind spot is the most important practical reason to use variable functions and dynamic method calls sparingly: every such call is a region of code that static analysis tools can no longer fully verify, regardless of how high the configured PHPStan level is. A refactoring that renames a method silently goes unnoticed at this point, whereas the same rename on a direct, literal method call would immediately trigger an error even at level 0.
8. First class callable syntax as a type safe alternative
Since PHP 8.1, first class callable syntax with function(...) or $object->method(...) offers a type safe way to create a reference to a function or method without resorting to a string name. The decisive difference from variable functions: the method name remains a literal part of the source code, so IDEs, PHPStan and refactoring tools fully understand the call and automatically follow along on renames.
For dispatcher patterns where several possible actions are known in advance, this syntax can be combined with an array mapping command names to first class callables. This approach retains the flexibility of a data driven dispatcher without losing the type safety and traceability that variable functions and dynamic method calls give up.
<?php
declare(strict_types=1);
final class TypeSafeCommandDispatcher
{
/** @var array<string, Closure> */
private array $handlers;
public function __construct(private readonly OrderCommandHandler $handler)
{
// First-class callable syntax: method names stay literal in the source
$this->handlers = [
'create' => $this->handler->create(...),
'cancel' => $this->handler->cancel(...),
];
}
public function dispatch(string $command, array $payload): string
{
$callable = $this->handlers[$command]
?? throw new InvalidArgumentException("Unknown command: {$command}");
return $callable($payload);
}
}
$dispatcher = new TypeSafeCommandDispatcher(new OrderCommandHandler());
echo $dispatcher->dispatch('cancel', ['id' => 42]) . PHP_EOL; // Order cancelled: 42
9. Variable functions/variables compared to alternatives
The following table compares variable functions and variable variables to their respective modern alternatives.
| Use case | Low level approach | Modern alternative | Advantage of the alternative |
|---|---|---|---|
| Dispatching a known set of actions | $obj->$method() |
First class callable + array | Static analysis and refactoring remain possible |
| Setting several instance variables from form fields | $$name |
Explicit array with allowlist | No uncontrolled overwriting of variables |
| Data driven formatter selection | $fn() from string |
match expression or strategy object | IDE support and type checking preserved |
| Filling an object from unknown fields | $obj->$field |
ReflectionProperty with whitelist | Also reaches private properties, with clear error handling |
In almost every case, modern alternatives offer the same flexibility as variable functions and variable variables without taking on their biggest downside: the loss of static verifiability. Variable functions and variable variables remain relevant nonetheless, to understand how many older codebases and some generic libraries work internally.
Mironsoft
Legacy code modernization and type safe PHP architecture
Defusing risky dynamic calls in your code?
We identify variable functions and variable variables in existing codebases and replace them, wherever possible, with type safe alternatives that PHPStan can fully verify.
Legacy audit
Systematically tracking down risky dynamic calls
Safe refactoring
Migrating to first class callables and type safe dispatchers
PHPStan rollout
Establishing static analysis even for dynamically shaped legacy projects
10. Summary
Variable functions and variable variables are the oldest form of metaprogramming in PHP: a function name in $fn() or a variable name in $$name is only resolved from a string at runtime. Both mechanisms are powerful for data driven dispatchers and generic property access, but require consistent safeguarding with is_callable(), method_exists() and an allowlist of permitted names, to avoid fatal errors and security vulnerabilities.
The biggest downside is the loss of static verifiability: PHPStan and Psalm cannot check existence or signature for dynamically assembled names. Since PHP 8.1, first class callable syntax combined with an array replaces many classic use cases without giving up the type safety and IDE support that variable functions and variable variables lack.
Variable Functions and Variable Variables — The Essentials at a Glance
Variable functions
$fn() calls the function whose name is the string in $fn, always check with is_callable().
Variable variables
$$name accesses the variable whose name is stored in $name, use only internally and in a controlled way.
Static analysis limit
PHPStan cannot check existence or signature for dynamically assembled names.
Modern alternative
First class callable syntax since PHP 8.1 keeps method names literal and type safe.