Variable Functions and Variable Variables: Low Level Metaprogramming in PHP
AI generated
<?php
8.4
PHP · Dynamic Dispatch · Metaprogramming
Variable Functions and Variable Variables
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.

16 min read $fn() · $$name · call_user_func PHP 5.x to 8.4

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.

11. FAQ: Variable Functions and Variable Variables

1What is a variable function?
The function name lives in a variable, invoked with $name() instead of a literal function name.
2What is a variable variable?
$$name accesses the variable whose name is stored in the content of $name.
3Why an anti pattern?
Uncontrolled from user input, they could overwrite arbitrary variables, as once happened with register_globals.
4Safeguarding a call?
Check with is_callable(), and maintain an allowlist of permitted names for external sources.
5Why no PHPStan check?
The name only emerges at runtime, static analysis without execution cannot predict it.
6Difference from $obj->$method()?
Same principle, just applied to object methods instead of free functions.
7What is first class callable syntax?
function(...) since PHP 8.1 creates a type safe reference with a name that stays literal.
8Still legitimately usable?
Only internally and in a controlled way, never with direct user input.
9What if the name doesn't exist?
A fatal error, which is why is_callable() before every call is indispensable.
10Avoid entirely in new projects?
Mostly yes, in favor of match or first class callables, pragmatic only for open dispatchers with an allowlist.