Skip to content

Commit

Permalink
[TASK] Add Named Arguments best practices
Browse files Browse the repository at this point in the history
The TYPO3 core team decided on this document
regarding best practices for the adoption and
avoidance of this PHP language feature.
  • Loading branch information
lolli42 committed Mar 17, 2024
1 parent 7a11891 commit b70cecd
Show file tree
Hide file tree
Showing 2 changed files with 248 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ part is just an overview of common pitfalls and generic tips.
:maxdepth: 1
:titlesonly:

NamedArguments
AccessingTheDatabase
Singletons
StaticMethods
Expand Down
247 changes: 247 additions & 0 deletions Documentation/CodingGuidelines/CodingBestPractices/NamedArguments.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
.. include:: /Includes.rst.txt
.. index:: pair: Coding guidelines; Named Arguments
.. _cgl-named-arguments:

===============
Named Arguments
===============

`Named arguments <https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments>`__,
also known as “named parameters”, were `introduced in PHP 8 <https://wiki.php.net/rfc/named_params>`__,
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:

.. code-block:: php
:caption: Named arguments example
<?php
function createUser($username, $email)
{
// code to create user
}
createUser(email: '[email protected]', username: 'john');
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 <https://php.watch/versions/8.0/named-parameters>`__:

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 (see below).

* 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
------------------------------------------------

Recent advancements in the TYPO3 core codebase emphasize the separation of
functionality and state, leading to the broad utilisation of value objects.
Consider the following example:

.. code-block:: php
:caption: 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);
}
}
Using public constructor property promotions (PCPP) facilitates object
initialization, representing one of the primary use cases for named arguments
envisioned by PHP developers:

.. code-block:: php
:caption: Instantiate a PCPP value object using named arguments
$label = new Label(
label: $myLabel,
color: $myColor,
priority: -1,
);
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 Dependency Methods
-------------------------------------

The TYPO3 core refrains from employing named arguments when calling library
code 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 extensions

* 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:

.. code-block:: php
:caption: 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!
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 :php:`languageKey` and
:php:`expectedLabels` as named arguments in the test:

.. code-block:: php
:caption: 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
}
}
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 :php:`json_decode()` has this signature:

.. code-block:: php
:caption: json_decode() function signature
json_decode(
string $json,
?bool $associative = null,
int $depth = 512,
int $flags = 0
): mixed
In many cases, the arguments :php:`$associative` and :php:`$depth` suffice with their
default values, while :php:`$flags` typically requires :php:`JSON_THROW_ON_ERROR`.
Using named arguments in this scenario, bypassing the default values, results in a
clearer and more readable solution:

.. code-block:: php
:caption: Calling json_decode() using named arguments
json_decode(json: $myJsonString, flags: JSON_THROW_ON_ERROR);
Another instance arises with complex functions like :php:`preg_replace()`, where
developers often overlook argument positions and names:

.. code-block:: php
:caption: 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
);

0 comments on commit b70cecd

Please sign in to comment.