rfc:use_global_elements

This is an old revision of the document!


PHP RFC: declare(function_and_const_lookup=global)

Introduction

When calling a function or using a constant that isn't fully qualified from a namespace (other than the global namespace), PHP will check these two locations in order:

  1. The current namespace.
  2. The global namespace. (if not found in the current namespace)

This leads to the following problems:

  1. A minor decrease in performance, due to not being able to use specialized opcodes (and other optimizations) for functions such as strlen due to the ambiguity, not being able to evaluate the values of constants,
    and due to usually needing to check the current namespace before finding a function/constant in the global namespace at runtime.

    For example, version_compare(PHP_VERSION, '7.0.0', '>=') can't be converted to a constant by opcache in a namespace,
    but \version_compare(\PHP_VERSION, '7.0.0', '>=') can.
  2. Developers having to deal with ambiguity of which function is being called, if a project has or uses namespaced functions.

In order to eliminate the ambiguity, there are currently several options, which have different drawbacks:

  1. Add multiple use function function_name and use const MY_CONST at the top of the namespace.
    This is prone to merge conflicts when functions start/stop being used, inconvenient to keep up to date, and the vast majority of these will be global functions and constants.
  2. Write functions as \function_name() and constants as \MY_CONST.
    • This is more verbose, and inconvenient if the only types of constants/functions used are global.
    • Some builtins such as __DIR__ and empty() aren't actually constants/functions, and they can't be prefixed by backslashes.
    • Version control history for lines would change if MY_CONST was replaced with \MY_CONST
    • New contributors to a project would often be unfamiliar with any applicable coding style guideline of fully qualifying function/const uses.
  3. Avoid using namespaces (this is forbidden by PSR-1)
  4. Mixes of the above approaches.

This RFC proposes a new language feature which avoids those drawbacks.

Proposal

Support declare(function_and_const_lookup = global) as the first statement of a PHP file (the same place as strict_types). This directive can be used in combination with function/global uses.

namespace MyNS;
declare(
    strict_types=1,
    function_and_const_lookup=global
);
use function OtherNS\my_function;
use const OtherNS\OTHER_CONST;
 
// The below function and constant references now unambiguously refer to the global namespace.
//
// Without "declare(function_and_const_lookup=global) ...",
// php would have checked for MyNS\version_compare
// and MyNS\PHP_VERSION at runtime before checking the global namespace.
if (version_compare(PHP_VERSION, '8.0.5') >= 0) {
    // ...
}

Implementation Details

declare(function_and_const_lookup=global) can be thought of behaving the same way as if every possible function name and constant name was used from the global namespace (for names without use elsewhere). A difference is that it allows declaring a function or constant, but does not add the use.

  • Note that declaring a function or constant within a file does not add a use.
declare(function_and_const_lookup=global);
 
namespace MyNS;
 
function sprintf($msg, ...$args) {
    // this and remaining statements still refer to \sprintf, not MyNS\sprintf
    return sprintf("Prefix $msg", ...$args);  
}
 
// You must explicitly indicate that the function you want to use is in the current namespace,
// e.g. with 'use function MyNS\factorial' or by invoking it as 'namespace\factorial()'
use function MyNS\factorial;
function factorial(int $n) {
    return $n > 1 ? factorial($n - 1) * $n : 1;
}

There is a working implementation at https://github.com/php/php-src/pull/4951

function_and_const_lookup has two possible values: global and default. default is the same as omitting the setting, and is supported in case it is desirable for future language changes such as https://wiki.php.net/rfc/namespace_scoped_declares

Error handling

The error handling (for the first voting options) can be thought of being the same as if every possible function name and constant name was used from the global namespace (for names not used elsewhere).

  • Declaring a function or constant in a file with function_and_const_lookup=global is allowed without notices or errors.
    define() is not affected.

    This RFC does not propose triggering notices or errors, because forbidding declaring functions or constants may cause problems with future work such as https://wiki.php.net/rfc/namespace_scoped_declares , and would prevent using this directive in all files.
  • A warning is emitted if function_and_const_lookup=global and use <element_type> global_element_name are both used, due to the latter being redundant when referring to the global namespace.
  • declare(function_and_const_lookup=global) does not cause notices if the any of the below namespaces are the global namespace.

Backward Incompatible Changes

This version of the RFC has no backwards incompatible changes. The behavior of existing code is unchanged.

Proposed PHP Version(s)

8.0

Proposed Voting Choices

Yes/No Vote

(Requires 2/3 vote to pass)

Severity of redundant uses

When declare(function_and_const_lookup=global) and use <element_type> global_element_name; are both used in a namespace block, due to the latter being redundant when referring to the global namespace, php will do the following when the file is required:

  1. Warning (as described in the RFC)
  2. Fatal Error
  3. Allow it and don't warn

If “Allow it and don't warn” has over 50% of the votes, that option wins.

Otherwise, the option of “Warning” or “Fatal Error” with the most votes wins.

(Note that explicitly adding use <element_type> global_element_name; to a php file has the effect of preventing a function/const from being declared with the same name but a different namespace (the current namespace) at compile time. I don't expect that to be a common practice.)

Syntax

  1. Unquoted: (function_and_const_lookup = global), (function_and_const_lookup = default)
  2. Quoted: (function_and_const_lookup = 'global'), (function_and_const_lookup = 'default')

Background: There are 3 declare options so far: encoding=“UTF-8”, ticks=500, and strict_types=1. See https://www.php.net/manual/en/control-structures.declare.php

Arguments for a string literal: encoding=“UTF-8” is quoted, and every declare directive currently only allows scalar values. This would keep the parsing simple and unchanged. Arguments for a keyword: Keys aren't quoted, there are only two possible values for function_and_const_lookup, and encoding needs to be quoted because of the hyphen. Also, this is shorter.

Patches and Tests

Discussion

Arguments for declare() syntax

From https://externals.io/message/107877#107883

That [use global functions] still doesn't really explain what's happening, because in code that doesn't use any namespaced functions, the line has no user-visible effect at all - functions are always used from the global namespace. What it actually does is switch off a language feature, so perhaps it should be something more like:

declare(lookup_functions_in_current_namespace=false);

That would also mean that it can be covered by any future way of setting language directives, such as settings per “module”, bundling into “editions”, etc.

From https://externals.io/message/107877#107894

  1. strict_types is not the first or only declare directive. Declaring an encoding fundamentally changes the meaning of a source file; probably it would be invalid in a different encoding, but errors are not the primary purpose of the directive. Declaring a value of “ticks” actually changes the runtime behaviour of the code. The manual defines declare vaguely as “used to set execution directives”, so it's not particularly clear to me that changing namespace resolution would be outside of its purpose.
  2. The existing fallback to global scope means that looking at the use statements of a file is not sufficient to resolve the ambiguity of an unprefixed function name. Indeed, the same line of code can execute two different functions within a running program if the namespaced function is defined at the right time.

From https://externals.io/message/107877#107894

I'm generally not convinced that beginning the special directive with the word “use” automatically makes it easier to find or understand. Given some code mentioning “strpos”, you wouldn't be able to scan the list of use statements for the word “strpos”, you'd have to understand that there are two modes of execution, and look for a line that switches between those modes.

From https://externals.io/message/107953#108132

First and foremost, if we ever implement https://wiki.php.net/rfc/namespace_scoped_declares or some similar way of specifying declares on the package level, and I think it's pretty likely that we're going to do this in one form or another, then we're very much going to regret not making this a declare. Disabling the namespace fallback, just like the use of strict types, is something you will usually want to do for an entire library/project, not just for individual files. Going for something like “use global functions” preemptively precludes this for no good reason.

Arguments for use global functions/consts syntax

  • This is similar to the use function ...; syntax. Both use global functions and declare(...) make it clear that this syntax can be used only for the global namespace.
  • use global functions; can be placed immediately adjacent to other use function ...; statements, so only one block of code needs to be checked for function/constant resolution behavior (if coding style guidelines are enforced).
  • This can be set per each namespace block, in the uncommon case of files that combine blocks from multiple namespaces. This makes it possible for a file to use this in some namespace blocks, but also declare functions/constants in different namespace blocks. (The latest RFC version allows declaring functions/constants with declare(function_and_const_lookup=global), so this argument no longer applies)

(The first versions of this RFC used the syntax use global functions; and use global consts)

An argument against the separate statements for functions and constants is that these two directives would almost always be combined, so it would make to use a combination of these.

Deprecate the fallback to the root namespace instead?

An earlier RFC https://wiki.php.net/rfc/fallback-to-root-scope-deprecation had the following notable objections, and I'm not sure of the current status of that RFC:

  • Deprecating the behavior of existing code would break (possibly unmaintained) third-party libraries and require a lot of code changes, discouraging updates to the latest version of PHP.

    use global functions; wouldn't.
  • Some mocking libraries will declare functions in namespaces being tested, with the same name as the global function.
    My proposal has similar drawbacks - third party code that started using “use function *” would break those libraries (but so would manually adding the same namespace uses).

    But there are alternatives to those libraries, such as uopz, runkit7, and SoftMocks (SoftMocks changes the class autoloader to replace code with instrumented code).

Any work on that RFC can be done independently of this RFC.

Encourage static methods and class constants instead

Arguments for: - Classes don't have the ambiguity functions and constants do, and already have autoloaders, so we should encourage use of classes instead. - Can potentially support use function Ns\SomeClass::some_method;, use const Ns\SomeClass::SOME_CLASS_CONST instead

Arguments against: - PHP will still need to support existing php modules and userland libraries/polyfills that use global functions/constants.

Deprecate fallback to the root namespace instead?

(i.e. for unqualified my_function(), do the opposite of this: work an approaches to only check \NS\my_function() without checking \my_function() to avoid ambiguity.)

This was suggested in combination with other strategies. See the section “Use module/library systems instead”.

Use module/library systems instead

We might eventually benefit from versioned “libraries” of functions that can be imported in one command which would solve many-a-future-problem by itself.

I'd be in favor of a module system, I just can't think of a good way to migrate to that with existing use cases and limitations. I'd imagine there would be various obstacles/objections to getting an RFC for that approach accepted. (continuing to support polyfills and autoloading; increasing ambiguity instead of decreasing ambiguity; any implementations I can think of would have worse performance with opcache)

If it is implemented in a way causing large backwards compatibility breaks, it may discourage upgrading.

See https://externals.io/message/107953#107962 for more details.

The performance hit is minor

It is in most cases. Counterarguments:

  • Making it easier to avoid ambiguity has benefits elsewhere - for analyzing code, future work on autoloading, helping developers avoid unexpected behavior in edge cases in applications, etc.
  • It's much larger in some cases than others due to other opcache optimizations, frequency of calling a line of code, etc. Benchmarking and interpreting performance of applications after new features get added may take more developer time than ensuring files contain declare(function_and_const_lookup=global);
  • -Developers may still aim to avoid ambiguity without this RFC, resulting in more verbose code.

Implementation

References

rfc/use_global_elements.1579965212.txt.gz · Last modified: 2020/01/25 15:13 by tandre