rfc:namespace_scoped_declares

This is an old revision of the document!


PHP RFC: Namespace-scoped declares

  • Version: 0.9
  • Date: 2016-09-20
  • Author: Nikita Popov nikic@php.net
  • Proposed PHP version: PHP 7.2
  • Status: Draft

Introduction

PHP 7 introduced the strict_types declare directive, which controls how scalar type declarations behave in a certain file. A common complaint is that this directive has to be specified in every single file. This RFC proposes the addition of namespace-scoped declares, which allows specifying a default declare directive for an entire namespace. While this improves the usability of strict types specifically, this feature could also form the basis to opt-out of certain undesirable language semantics, without breaking library-interoperability or backwards compatibility.

Here is an example of how this functionality could look like:

// bootstrap.php
namespace_declare('Vendor\Lib', ['strict_types' => 1]);
namespace_declare('Vendor\Lib\Sub\Name\Space', ['strict_types' => 0]);

The first call would make strict_types=1 the default in the namespace Vendor\Lib and all sub-namespaces (i.e. all namespaces starting with Vendor\Lib\). The second call would create an exception for this and make strict_types=0 the default in Vendor\Lib\Sub\Name\Space (and sub-namespaces). A specific file in this namespace could then again override this default using a specific declare(strict_types=1); directive.

Motivation and Vision

The motivation behind this proposal is broader than just saving a single declare() in each file of a project. This kind of namespace-scoped declare may provide the means for introducing certain changes that make the language “stricter” without breaking backwards compatibility or library-interoperability (as ini directives do).

For example, in modern PHP code it is nearly always a bug to dynamically create a property on an object that has not previously been declared. However disallowing this globally would be completely unthinkable, because a lot of software relies on it, and some use-cases legitimately benefit from the ability to do this. Adding an ini-setting for this is not possible either, because this would prevent using libraries that assume different values for this setting (which is the same reason why strict_types is not an ini setting).

Using a declare() directive for this purpose would solve the inter-operability issues and would additionally provide an “escape-hatch” for the cases the functionality is legitimately needed. However if the number of such directives increases, specifying them for every single file in a project would quickly become unwieldy. Additionally, it is likely that a library will want to use the same directives for all files. Changing the value of a directive would then require performing an update on all files. Namespace-scoped declares solve this by specifying such options in a single bootstrap file:

// bootstrap.php
namespace_declare('Vendor\Lib', [
    'strict_types' => 1,
    'dynamic_object_properties' => 0,
    ...
]);

For libraries using Composer this bootstrap file would be specified as an autoloader of type “file”. However, if this kind of approach should prove popular, it might be possible to include this information directly in the composer.json and let Composer manage it:

{
    "declares": {
        "Vendor\\Lib\\": {
            "strict_types": 1,
            "dynamic_object_properties": 0
        }
    }
}

Proposal

The introduction of a namespace_declare function with the following signature is proposed:

function namespace_declare(string $namespace, array $directives) : void;

The given $directives will be used as default values for all code inside the namespace $namespace or a sub-namespace. A sub-namespace is any namespace that starts with $namespace . “\\”. (Note that namespaces are case-insensitive, so namespace_declare also applies case-insensitively.)

If the provided $namespace is the empty string, an Error is thrown. It is explicitly not possible to specify a global default.

If namespace_declare was already called for a certain $namespace, another call with the same namespace will result in an Error.

The declare directive defaults specified by namespace_declare() take effect for all files compiled (i.e. included) after the execution of the function call. Files included prior to the function call are not affect. The file that contains the call is also not affected, because the function will only be executed after the file has already been compiled.

Nesting behavior

If namespace_declare has been called for namespaces A and A\B, where A\B is a sub-namespace of A, then the directive defaults for A\B take precedence before those of A. Consider the following example:

namespace_declare('A', ['strict_types' => 1, 'no_dynamic_properties' => 1]);
namespace_declare('A\B', ['no_dynamic_properties' => 0]);

Given the above calls, code in namespace A\B will have the following declare directives active (assuming they are not overwritten with declare()):

no_dynamic_properties=0 // Because this was specified on A\B
strict_types=1          // Because this was not specified on A\B, but specified on A
ticks=0                 // Because this was not specified on A\B or A, so the global default is used
...

The nesting behavior does not depend on the order in which namespace_declare is called. As such the following code results in the same behavior:

namespace_declare('A\B', ['no_dynamic_properties' => 0]);
namespace_declare('A', ['strict_types' => 1, 'no_dynamic_properties' => 1]);

Open Question: Handling of unknown directives

The declare() statement will throw a compile-time warning if an unknown directive is used. This has the advantage that typos can be easily spotted. On the other hand this is bad for forward-compatibility, because specifying a directive that is only known in newer PHP versions would generate a warning in older versions. If the directive only exists to make certain checks stricter (such as the dynamic_object_properties example) it would be preferably it would simply be (silently) ignored on older versions.

If we go for throwing a warning here, it might be beneficial to add a supports_declare() function, which allows you to determine whether a certain declare directive is supported, without resorting to a PHP version check (which would likely be unreliable with regard to alternative PHP implementations).

Open Question: namespace_declare after namespace already used

As specified above, it is possible to first load code in a certain namespace and subsequently call namespace_declare(). In this case the already compiled code will not be affected by the namespace_declare(), which is bound to cause confusion.

To avoid this, namespace_declare() could throw an Error if it called after code in the given namespace (or a sub-namespace) has already been loaded. However, this would require us to internally track which namespaces have had code loaded, something which we currently don't do. This tracking would also require opcache support.

Open Question: Files with mixed namespaces

PHP allows mixing multiple namespaces in a single file. This is problematic for the feature being proposed, because the different namespaces could have different namespace-scoped declares:

// bootstrap.php
namespace_declare('A', ['strict_types' => 1]);
namespace_declare('B', ['strict_types' => 0]);
 
// some_file.php
namespace A; // Is strict_types=1
...
 
namespace B; // Is strict_types=0
...

Not only is this very confusing, because this means that language behavior can silently change in the middle of a file, but it also poses a significant implementational problem. For example the current implementation of strict_types only permits a consistent strict_types value for a whole file. Allowing this kind of code would require changing the strict_types implementation to support this and, more importantly, commit to support this for all future declare directives we introduce, which may be non-trivial.

Instead it may be preferable to only allow mixing multiple namespaces in the same file if they have consistent declare directive defaults. This still allows mixing namespaces, but enforces that declare directives will not magically change throughout the file.

TODO: What about the encoding directive

Challenges, Disadvantages, Alternatives

Technical challenges

There are a number of technical challenges involved in the implementation of this feature:

  • If the open question “namespace_declare after namespace already used” is resolved in favor of forbidding such uses, information about namespaces that have already been loaded needs to be stored by the runtime. This requires opcache integration. (I consider this resolution likely.)
  • If the open question “Files with mixed namespaces” is resolved in favor of allowing such uses, the strict_types implementation will have to be significantly changed to allow changing strict_types in the middle of a file. (I consider this resolution unlikely.)
  • In any case, namespace-scoped declares will have to be taken into account by opcache. Namely, if a file is compiled with a certain set of namespace-scoped declares, it cannot necessarily be reused if it is compiled with a different set of declares. This could be solved by storing a checksum based on the namespace-scoped declares at the time of compilation together with the cached file and compare it when loading it. I believe this can be done efficiently.

Disadvantages

It could be argued that the introducing of namespace-scoped declares will make it less discoverable which declare values are active inside a given file, because they are no longer required to be specified at the top of the file.

This if course correct, in that namespace-scoped declares are by necessity less explicit. However I believe that this is unlikely to cause confusion as long as libraries and projects stick to the simple best-practice of specifying all namespace-scoped declares inside a single file. Composer integration could further help mitigate this issue.

Additionally it should be noted that the same problem (to an even worse degree) exists for the ini system, where ini options can be specified from many different sources (including multiple ini files, htaccess files and inline in PHP code). In practice this does not appear to be a major problem.

Alternatives

The main alternative to this proposal I see is the introduction of a “proper module system”, which would allow per-module specification of declares, similar to package attributes in Java. However this is just a very vague concept in my mind and would certainly require major language changes.

Backward Incompatible Changes

None.

Proposed Voting Choices

As this is a language change, a 2/3 majority is required.

Patches and Tests

There is no implementation yet.

rfc/namespace_scoped_declares.1474403638.txt.gz · Last modified: 2017/09/22 13:28 (external edit)