WordPress.org

Make WordPress Core

Opened 2 years ago

Last modified 17 months ago

#42264 new enhancement

Systematic way of dealing with compat code and polyfills

Reported by: schlessera Owned by:
Milestone: Awaiting Review Priority: normal
Severity: normal Version:
Component: Bootstrap/Load Keywords:
Focuses: Cc:
PR Number:

Description (last modified by westonruter)

The way compatibility code and fallback/polyfill functionality is currently handled has a few issues:

  • As everything resides in one big file, all of the code is parsed every time.
  • As everything resides in one file, problems like the PHP 7.2 parsing error for the autoload polyfill can crop up (as the polyfill is written with now deprecated code).
  • If the requirements change, it is non-trivial to remove unneeded code again.

I'd like the suggest a more systematic way of loading the compatibility layer. The basic premise is that the PHP version of the current server is detected, and then an individual compatibility file is loaded for each version that is not already supported by the server.

This provides a clean way of structuring the compatibility layer, giving a good overview of what is needed when, and what can be discarded. It also only loads the code that is needed.

Here's the main mechanism that would make this work:

<?php
// Check the PHP version and load the corresponding compatibility files.
// The fall-throughs (missing breaks) are intentional, as this makes sure that
// all compat files - starting from the first required one - will be loaded.
switch ( substr( PHP_VERSION, 0, 3 ) ) {
        case '5.2':
                require ABSPATH . WPINC . '/compat/php-5.2.php';
        case '5.3':
                require ABSPATH . WPINC . '/compat/php-5.3.php';
        case '5.4':
                require ABSPATH . WPINC . '/compat/php-5.4.php';
        case '5.5':
                require ABSPATH . WPINC . '/compat/php-5.5.php';
        case '5.6':
                require ABSPATH . WPINC . '/compat/php-5.6.php';
        case '7.0':
                require ABSPATH . WPINC . '/compat/php-7.0.php';
        case '7.1':
                require ABSPATH . WPINC . '/compat/php-7.1.php';
        case '7.2':
                require ABSPATH . WPINC . '/compat/php-7.2.php';
        default:
                require ABSPATH . WPINC . '/compat/default.php';
}

Note the fall-throughs of the case statements. As an example, if the current server would be running PHP 5.6, the above mechanism would load the compatibility files for 5.6, 7.0, 7.1 and 7.2.

Inside of the individual files, you'd have fallbacks and polyfills, that are needed for the version it resides in and all previous versions.

Attachments (1)

42264.1.diff (33.9 KB) - added by schlessera 2 years ago.
Sample implementation of the discussed concept

Download all attachments as: .zip

Change History (8)

@schlessera
2 years ago

Sample implementation of the discussed concept

#1 @westonruter
2 years ago

  • Description modified (diff)

#2 @ayeshrajans
19 months ago

Hi everyone - is there any indication if this approach will be used in core? If that's the case, I'm willing to dedicate my time to wrote the polyfills (where possible to do in userland code of course) and maintain it.

#3 @jrf
19 months ago

@schlessera I've been thinking about this a little more and while I support the basic premise of this proposal - splitting out the back-compat functionality into several files which are selectively loaded -, I don't think the currently proposed logic would work.

A significant number of the back-fills are related to PHP extensions - in contrast to the PHP Core -.
Depending on the PHP version, certain extensions ship with PHP by default. However, this is no guarantee that those extensions will actually be available in that PHP version or not available in versions where the extension was not (yet) included by default.

  • Webhosts, or any PHP installer for that matter, do not have to use the default distribution.
  • Some webhosts backport security fixes to old PHP versions and at times, these fixes may contain functionality which has been backfilled.
  • A custom PHP compilation using PECL extensions may be used.
  • etc etc

So the fact that WP is running on a system running PHP 7.2, is no guarantee that the Sodium extension is available, PHP 5.x is no guarantee that bcmath is available (shipped with PHP since 4.0.4), and running PHP 7.0 is no guarantee that Sodium will *not* be available.

Over the last x years I've been amazed (and frustrated) more times than I can count by bug reports for plugins where it turned out that a non-default PHP install missing some essential extension was the underlying cause of the issue.

So, having said that, I think a more extension based approach is needed here using extension_loaded() to check whether, for instance, the back-fills related to MBString need to be loaded or not.

Presuming all back-fill related files will live in a wp-includes/phpcompat directory, I imagine the loading mechanism could be something along the lines of (pseudo-code, untested, just to demonstrate the idea):

<?php
$php_backfills = array(
        'CSPRNG',
        'MbString',
        'Sodium',
        'Curl',
);

require_once ABSPATH . WPINC . '/phpcompat/core-functions-and-constants.php';

foreach ( $php_backfills as $extension ) {
        if ( extension_loaded( $extension ) === false ) {
                require_once ABSPATH . WPINC . '/phpcompat/' . $extension . '.php';
        }
}

Where necessary, this could be enhanced with a call to phpversion( $extension ) to check that the extension complies with a minimum required version of that extension.

Last edited 19 months ago by jrf (previous) (diff)

#4 @ayeshrajans
19 months ago

👍👌 for extension_loaded calls to detect availability. That really makes the code more readable too.
<bike-shedding>
There are few arguments in outside PHP communities to use function_exists vs extension_loaded - function_exists is tiny bit faster and functions can be disabled in php.ini where extension_loaded would happily return true, but function_exists doesn't.

</bike-shedding>

#5 @jrf
19 months ago

@ayeshrajans All individual back-fills should still be wrapped in function_exists(), interface_exists(), defined() calls, just like they are now.

The extensions_loaded() logic example was just intended to regulate which back-fill files to load.

and functions can be disabled in php.ini where extension_loaded would happily return true, but function_exists doesn't.

You have a point there which needs further thought, as that would be a argument to have the compat bootstrap file have all the function_exists() checks and have the back-fill files just add individual functions, though that would also necessitate each back-fill to be in their own file, which might be going over the top a bit with modular loading.

#6 @schlessera
17 months ago

@jrf The intent of the ticket was not necessarily to provide a more granular way to load the backcompat code, as it was to make maintenance easier with regards to how we hopefully manage to more closely match the PHP update velocity in the long run.

For extensions that are optional, you will always have to at least load the code that will check whether the extension is active and can be used. The check might even sometimes be complicated, as even extensions are not always compiled in the same way.

For extensions that have become required at one point, you always load the polyfill except for when you've reached the minimum PHP version where it became standard.

The setup I suggested basically makes it very easy to get rid of redundant code every time we bump the PHP minor version. So, when we bump PHP minimum version from PHP 5.2 to PHP 5.3 hopefully soon, we just delete the /compat/php-5.2.php file and that's it. PHP is moving faster and faster, and collecting a huge amount of compat code without a mechanism to get rid of it as needed, and without doubts about what to remove seems useful.

I see no reason why we couldn't combine both approaches, to have PHP-version-based files and extension-based files.

#7 @jrf
17 months ago

I see no reason why we couldn't combine both approaches, to have PHP-version-based files and extension-based files.

@schlessera Agreed 👍.

For extensions that have become required at one point, you always load the polyfill except for when you've reached the minimum PHP version where it became standard.

My point was more that this statement can not be relied on as PHP can be (and I've seen it happen) compiled without the required extensions.

Note: See TracTickets for help on using tickets.