Named arguments

Named arguments, also known as “named parameters”, were introduced in PHP 8, offering a new approach to passing arguments to functions. Instead of relying on the position of parameters, developers can now specify arguments based on their corresponding parameter names:

Named arguments example
<?php
function createUser($username, $email)
{
    // code to create user
}
createUser(email: 'john.doe@example.com', username: 'john');
Copied!

This document discusses the use of named arguments within the TYPO3 Core ecosystem, outlining best practices for TYPO3 extension and Core developers regarding the adoption and avoidance of this language feature.

Named arguments in public APIs

The key consideration when using this feature is outlined in the PHP documentation:

With named parameters, the name of the function/method parameters become part of the public API, and changing the parameters of a function will be a semantic versioning breaking-change. This is an undesired effect of named parameters feature.

Utilizing named arguments in extensions

While the TYPO3 Core cannot directly enforce or prohibit the use of named arguments within extensions, it suggests certain best practices to ensure forward compatibility:

  • Named arguments should only be used for initializing value objects using PCPP (public constructor property promotion).
  • Avoid named arguments when calling TYPO3 Core API methods unless dealing with PCPP-based value objects. The TYPO3 Core does not treat variable names as part of the API and may change them without considering it a breaking change.

TYPO3 Core development

The decision on when to employ named parameters within the TYPO3 Core is carefully deliberated and codified into distinct sections, each subject to scrutiny within the Continuous Integration (CI) pipeline to ensure consistency and integrity over time.

It’s important to note that the TYPO3 Core Team will not accept patches that aim to unilaterally transition the codebase from positional arguments to named arguments or vice versa without clear further benefits.

Leveraging Named Arguments in PCPP Value Objects

Advancements in the TYPO3 Core codebase emphasize the separation of functionality and state, leading to the broad utilization of value objects. Consider the following example:

Value object using public constructor property promotion
final readonly class Label implements \JsonSerializable
{
    public function __construct(
        public string $label,
        public string $color = '#ff8700',
        public int $priority = 0,
    ) {}

    public function jsonSerialize(): array
    {
        return get_object_vars($this);
    }
}
Copied!

Using public constructor property promotions (PCPP) facilitates object initialization, representing one of the primary use cases for named arguments envisioned by PHP developers:

Instantiate a PCPP value object using named arguments
$label = new Label(
    label: $myLabel,
    color: $myColor,
    priority: -1,
);
Copied!

Objects with such class signatures MUST be instantiated using named arguments to maintain API consistency. Standardizing named argument usage allows the TYPO3 Core to introduce deprecations for argument removals seamlessly.

Invoking 2nd-party (non-Core library) dependency methods

The TYPO3 Core refrains from employing named arguments when calling library code ("2nd-party") from dependent packages unless the library explicitly mandates such usage and defines its variable names as part of the API, a practice seldom observed currently.

As package consumer, the TYPO3 Core must assume that packages don’t treat their variable names as API, they may change anytime. If TYPO3 Core would use named arguments for library calls, this may trigger regressions: Suppose a patch level release of a library changes a variable name of some method that we call using named arguments. This would immediately break when TYPO3 projects upgrade to this patch level release due to the power of semantic versioning. TYPO3 Core must avoid this scenario.

Invoking Core API

Within the TYPO3 Core, named arguments are not used when invoking its own methods. There are exceptions in specific scenarios as outlined below, however these are the reasons for not using named arguments:

  • TYPO3 Core tries to be as consistent as possible
  • Setting a good example for extension authors
  • Avoiding complications and side effects during refactoring
  • Addressing legacy code within the TYPO3 Core containing methods with less-desirable variable names, aiming for gradual improvement without disruptions
  • Preventing issues with inheritance, especially in situations like this:

    PHP error using named arguments and inheritance
    interface I {
        public function test($foo, $bar);
    }
    
    class C implements I {
        public function test($a, $b) {}
    }
    
    $obj = new C();
    
    // Pass params according to I::test() contract
    $obj->test(foo: "foo", bar: "bar"); // ERROR!
    Copied!

Utilizing named arguments in PHPUnit test data providers

The use of named arguments in PHPUnit test data providers is permitted and encouraged, particularly when enhancing readability. Take, for example, this instance where PHPUnit utilizes the array keys languageKey and expectedLabels as named arguments in the test:

PHPUnit data provider using named arguments
final class XliffParserTest extends UnitTestCase
{
    public static function canParseXliffDataProvider(): \Generator
    {
        yield 'Can handle default' => [
            'languageKey' => 'default',
            'expectedLabels' => [
                'label1' => 'This is label #1',
                'label2' => 'This is label #2',
                'label3' => 'This is label #3',
            ],
        ];
        yield 'Can handle translation' => [
            'languageKey' => 'fr',
            'expectedLabels' => [
                'label1' => 'Ceci est le libellé no. 1',
                'label2' => 'Ceci est le libellé no. 2',
                'label3' => 'Ceci est le libellé no. 3',
            ],
        ];
    }

    #[DataProvider('canParseXliffDataProvider')]
    #[Test]
    public function canParseXliff(string $languageKey, array $expectedLabels): void
    {
        // Test implementation
    }
}
Copied!

Leveraging named arguments when invoking PHP functions

TYPO3 Core may leverage named arguments when calling PHP functions, provided it enhances readability and simplifies the invocation. It is allowed for functions with more than three arguments. If named arguments are used, all arguments must be named, mixtures are not allowed.

Let’s do this by example. Function json_decode() has this signature:

json_decode() function signature
json_decode(
    string $json,
    ?bool $associative = null,
    int $depth = 512,
    int $flags = 0
): mixed
Copied!

In many cases, the arguments $associative and $depth suffice with their default values, while $flags typically requires JSON_THROW_ON_ERROR. Using named arguments in this scenario, bypassing the default values, results in a clearer and more readable solution:

Calling json_decode() using named arguments
json_decode(json: $myJsonString, flags: JSON_THROW_ON_ERROR);
Copied!

Another instance arises with complex functions like preg_replace(), where developers often overlook argument positions and names:

Calling preg_replace() using named arguments
$configurationFileContent = preg_replace(
    pattern: sprintf('/%s/', implode('\s*', array_map(
        static fn($s) => preg_quote($s, '/'),
        [
            'RewriteCond %{REQUEST_FILENAME} !-d',
            'RewriteCond %{REQUEST_FILENAME} !-l',
            'RewriteRule ^typo3/(.*)$ %{ENV:CWD}typo3/index.php [QSA,L]',
        ]
    ))),
    replacement: 'RewriteRule ^typo3/(.*)$ %{ENV:CWD}index.php [QSA,L]',
    subject: $configurationFileContent,
    count: $count
);
Copied!