Make WordPress Core

Opened 6 weeks ago

Last modified 6 weeks ago

#64830 new enhancement

Introduce a helper function to extract the major.minor "branch" version

Reported by: apermo's profile apermo Owned by:
Milestone: Awaiting Review Priority: normal
Severity: normal Version: 3.2
Component: General Keywords: has-patch needs-refresh needs-testing
Focuses: Cc:

Description

In PR https://github.com/WordPress/wordpress-develop/pull/11206 we ran into the issue that getting a patchless WP version is cluttered throughout core and sparked a discussion about introducing a proper helper function for extracting the major.minor (x.y) "branch" version.

The original problem

PHPStan flags (float) get_bloginfo('version') in admin-header.php:194 because str_replace() receives a float instead of a string. The PR author initially proposed changing the cast to (string), but this broke the existing behavior (the branch- CSS class would include the full version instead of just major.minor).

Key feedback

  1. @apermo caught the behavior regression and pointed out that the (float) cast is intentional — it strips the patch version (e.g., 6.9.1 → 6.9). He proposed get_bloginfo('branch_version') or a helper function.
  2. @siliconforks identified a genuine bug: the (float) cast is not reliably defined. If PHP's precision ini setting is non-default (e.g., 50), version 6.9.1 produces branch-6-9000000000000003552713678800500929355621337890625. This affects any version where the major.minor isn't exactly representable as a float (i.e., most versions except x.0).
  3. @westonruter noted the (float) approach is "abuse of a float" and proposed a string-based replacement using strtok/explode/array_slice. He also noted that for 7.0, the existing behavior produces branch-7 (not branch-7-0), and this should be preserved.
  4. @apermo cataloged 4 different approaches used across core to extract the x.y version, and proposed a single helper function using the robust implode/preg_split approach from class-core-upgrader.php that all locations could use.

Consensus direction

  • A helper function for extracting the branch version would benefit core, since at least 10 locations use varied (and sometimes fragile) approaches — some of which will break at version 10.0 (substr($ver, 0, 3))
  • The helper function should be a separate enhancement ticket from the immediate PHPStan fix

Affected locations

  • (float) cast: admin-header.php, class-wp-site-health-auto-updates.php, tests/basic.php
  • substr($ver, 0, 3): theme.php, plugin-install.php, block-template-utils.php, tests/basic.php
  • explode('.') with index: class-wp-site-health.php
  • implode/preg_split: class-core-upgrader.php (most robust)
<?php
 /**                                                                                                                                                                                                                                                              
   * Returns the major.minor "branch" version for a given WordPress version string.                                                                                                                                                                                
   *                                                                                                                                                                                                                                                               
   * Extracts the first two version components (major and minor) from a WordPress                                                                                                                                                                                  
   * version string, stripping any patch level and pre-release suffix.
   *
   * @since 7.1.0
   *
   * @param string $version Optional. A WordPress version string. Defaults to the
   *                        current WordPress version from wp_get_wp_version().
   * @return string The branch version string in "major.minor" format (e.g. "6.9").
   */
  function wp_get_branch_version( $version = '' ) {
        if ( '' === $version ) {
                $version = wp_get_wp_version();
        }

        $parts = preg_split( '/[.-]/', $version, 3 );

        return $parts[0] . '.' . ( $parts[1] ?? '0' );
  }

Decisions:

  • Reuses the preg_split('/[.-]/') approach from class-core-upgrader.php:282 — already proven in core and handles both . and - separators in one pass.
  • Accepts an optional $version parameter so callers like class-core-upgrader.php (which compares two versions) and class-wp-site-health.php can use it too, not just the current WP version.
  • Limit of 3 on preg_split() avoids unnecessary splits.
  • Always returns "major.minor" — e.g. "7.0" for "7.0-beta3", "6.9" for "6.9.1", "10.1" for "10.1.2-RC1".

Note: The existing admin-header.php behavior drops the .0 in the CSS class (producing branch-7 instead of branch-7-0). That's a quirk of the (float) cast. This is the main decision that needs to be taken which is the correct/expected behavior here.

Change History (9)

#1 @huzaifaalmesbah
6 weeks ago

If wp_get_branch_version() is introduced and returns major.minor (e.g. 7.0, 6.9), we could keep the current admin-header.php behavior (dropping the trailing .0) like this:

$admin_body_class .= ' branch-' . str_replace( '.', '-', preg_replace( '/\.0$/', '', wp_get_branch_version() ) );

This removes the (float) cast while preserving the existing branch-7 behavior.

This ticket was mentioned in PR #11211 on WordPress/wordpress-develop by @suhan2411.


6 weeks ago
#2

  • Keywords has-patch added

This introduces a new helper function wp_get_branch_version() for extracting the WordPress branch version (major.minor) from a version string.

Currently, several different approaches are used across core to determine the branch version, including:

  • (float) casting of get_bloginfo( 'version' )
  • substr( $ver, 0, 3 )
  • explode( '.' ) based parsing
  • preg_split() in class-core-upgrader.php

Some of these approaches are fragile or incorrect:

  • (float) casting can produce incorrect values due to floating-point precision issues.
  • substr( $ver, 0, 3 ) breaks for versions >= 10.
  • Multiple inconsistent approaches make maintenance harder.

This patch introduces wp_get_branch_version() using a string-based approach:

function wp_get_branch_version( $version = '' ) {
    if ( '' === $version ) {
        $version = wp_get_wp_version();
    }

    $parts = preg_split( '/[.-]/', $version, 3 );

    return $parts[0] . '.' . ( $parts[1] ?? '0' );
}

#3 @suhan2411
6 weeks ago

A pull request has been opened for review:

https://github.com/WordPress/wordpress-develop/pull/11211

This introduces the wp_get_branch_version() helper to extract the major.minor branch version safely and replaces existing fragile approaches (float cast, substr, etc.) across core.

Feedback welcome.

#4 follow-up: @jorbin
6 weeks ago

Please note that x.y is the major version, not major.minor. Minor is the third number. See: https://make.wordpress.org/core/handbook/about/release-cycle/version-numbering/

#5 in reply to: ↑ 4 @apermo
6 weeks ago

Replying to jorbin:

Please note that x.y is the major version, not major.minor. Minor is the third number. See: https://make.wordpress.org/core/handbook/about/release-cycle/version-numbering/

That pretty much sorts out what @westonruter said. So for any 7.0 Version including 7.0.0 the result has to be 7.0 and not 7 as it currently is in the admin header.

#6 @jorbin
6 weeks ago

For the automated tests, I would encourage using a dataProvider and test at least the following:

input   -> output
7.0     -> 7.0   # Major ending with 0 and no minor
7.0.0   -> 7.0   # minor number zero
7.0.1   -> 7.0   # Minor with a major that ends in zero
7.0.10  -> 7.0   # Double Digit Minor with trailing zero
10.0.0  -> 10.0  # Double digit first part of major having a zero
100.1.0 -> 100.1 # Triple digit since we don't want to introduce a bug in 2050 something. 


Further, https://core.trac.wordpress.org/browser/trunk/tests/phpunit/tests/functions/isWpVersionCompatible.php has some of the various ways version numbers can be messed up that would be good to ensure are handled correctly here.

#7 @peterwilsoncc
6 weeks ago

If there is a need for this, I'd investigate adding a parameter to wp_get_wp_version() to allow it to be called to get the major (x.x), minor (x.x.x) and full (7.0-beta3-61849) versions. A new function seems to be overkill.

#8 in reply to: ↑ description @westonruter
6 weeks ago

Good idea to extend wp_get_wp_version() to get a subset of the version.

Replying to apermo:

Note: The existing admin-header.php behavior drops the .0 in the CSS class (producing branch-7 instead of branch-7-0). That's a quirk of the (float) cast. This is the main decision that needs to be taken which is the correct/expected behavior here.

In 6.9.1, the added classes are branch-6-9 and version-6-9-1. I'm not aware how these classes are used by plugins. But since they they were introduced in r17957 to fix #17496 in 3.2, I would think it's safe to assume that the intention was that the format would be branch-X-Y. That said, I see three plugins which currently have .branch-7, .branch-6, and .branch-4: https://veloria.dev/search/58a08df4-64a6-46a4-bd55-2db4705968c2 (news flash: the new WPdirectory!!)

The .branch-7 example comes from WPeMatico RSS Feed Fetcher. It seems we should include both branch-7 and branch-7-0 for correctness and back-compat.

#9 @suhan2411
6 weeks ago

  • Keywords needs-refresh needs-testing added

Thanks everyone for the feedback.

I've updated the PR and pushed the changes addressing the points raised above.

  1. Terminology (@jorbin, @apermo)

Docblocks now follow the version numbering handbook: x.y represents the major version and x.y.z the minor version. The helper returns the major version (e.g. 7.0).

  1. Extending wp_get_wp_version() (@peterwilsoncc, @westonruter)

Instead of introducing a new public API, wp_get_wp_version() now accepts an optional $part parameter:

  • wp_get_wp_version( 'major' ) → x.y (e.g. 7.0)
  • wp_get_wp_version( 'minor' ) → x.y.z (e.g. 7.0.1)
  • wp_get_wp_version() / wp_get_wp_version( 'full' ) → full version string (unchanged)

wp_get_branch_version() remains available for parsing arbitrary version strings, and wp_get_wp_version( 'major' ) delegates to it for the current version.

  1. Admin header classes (@apermo, @westonruter)

For versions like 7.0.x, the admin now outputs both:

  • branch-7-0
  • branch-7 (for back-compat with existing selectors)
  1. Test coverage (@jorbin)

Added PHPUnit tests in tests/phpunit/tests/functions/wpGetBranchVersion.php using a data provider to cover cases such as:

  • 7.0, 7.0.0, 7.0.1, 7.0.10
  • 10.0.0, 100.1.0
  • alpha/beta/RC suffixes

Additional tests were added for wp_get_wp_version( 'major' ), wp_get_wp_version( 'minor' ), and wp_get_wp_version( 'full' ).

  1. Site Health edge case (@siliconforks)

The previous-version calculation for dev builds now uses wp_get_branch_version() and correctly resolves 7.0 → 6.9.

PR: https://github.com/WordPress/wordpress-develop/pull/11211

Note: See TracTickets for help on using tickets.