Make WordPress Core

Changeset 57287


Ignore:
Timestamp:
01/15/2024 07:03:27 PM (3 months ago)
Author:
swissspidy
Message:

I18N: Cache list of language file paths in WP_Textdomain_Registry.

Loading a list of language file paths using glob() can be expensive if involving thousands of files.

Expands scope of WP_Textdomain_Registry to cache list of language file paths in object cache and provides a way to invalidate that cache upon translation updates. Plugins can clear the cache using calls such as wp_cache_delete( 'cached_mo_files_' . md5( $path ), 'translations' );

Props mreishus, swissspidy
Fixes #58919

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-textdomain-registry.php

    r56178 r57287  
    5252     *
    5353     * @since 6.1.0
     54     * @since 6.5.0 This property is no longer used.
    5455     *
    5556     * @var array
     57     *
     58     * @deprecated
    5659     */
    5760    protected $cached_mo_files = array();
     
    6568     */
    6669    protected $domains_with_translations = array();
     70
     71    /**
     72     * Initializes the registry.
     73     *
     74     * Hooks into the {@see 'upgrader_process_complete'} filter
     75     * to invalidate MO files caches.
     76     *
     77     * @since 6.5.0
     78     */
     79    public function init() {
     80        add_action( 'upgrader_process_complete', array( $this, 'invalidate_mo_files_cache' ), 10, 2 );
     81    }
    6782
    6883    /**
     
    136151
    137152    /**
     153     * Retrieves .mo files from the specified path.
     154     *
     155     * Allows early retrieval through the {@see 'pre_get_mo_files_from_path'} filter to optimize
     156     * performance, especially in directories with many files.
     157     *
     158     * @since 6.5.0
     159     *
     160     * @param string $path The directory path to search for .mo files.
     161     * @return array Array of .mo file paths.
     162     */
     163    public function get_language_files_from_path( $path ) {
     164        $path = trailingslashit( $path );
     165
     166        /**
     167         * Filters the .mo files retrieved from a specified path before the actual lookup.
     168         *
     169         * Returning a non-null value from the filter will effectively short-circuit
     170         * the MO files lookup, returning that value instead.
     171         *
     172         * This can be useful in situations where the directory contains a large number of files
     173         * and the default glob() function becomes expensive in terms of performance.
     174         *
     175         * @since 6.5.0
     176         *
     177         * @param null|array $mo_files List of .mo files. Default null.
     178         * @param string $path The path from which .mo files are being fetched.
     179         **/
     180        $mo_files = apply_filters( 'pre_get_language_files_from_path', null, $path );
     181
     182        if ( null !== $mo_files ) {
     183            return $mo_files;
     184        }
     185
     186        $cache_key = 'cached_mo_files_' . md5( $path );
     187        $mo_files  = wp_cache_get( $cache_key, 'translations' );
     188
     189        if ( false === $mo_files ) {
     190            $mo_files = glob( $path . '*.mo' );
     191            if ( false === $mo_files ) {
     192                $mo_files = array();
     193            }
     194            wp_cache_set( $cache_key, $mo_files, 'translations' );
     195        }
     196
     197        return $mo_files;
     198    }
     199
     200    /**
     201     * Invalidate the cache for .mo files.
     202     *
     203     * This function deletes the cache entries related to .mo files when triggered
     204     * by specific actions, such as the completion of an upgrade process.
     205     *
     206     * @since 6.5.0
     207     *
     208     * @param WP_Upgrader $upgrader   Unused. WP_Upgrader instance. In other contexts this might be a
     209     *                                Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or Language_Pack_Upgrader instance.
     210     * @param array       $hook_extra {
     211     *     Array of bulk item update data.
     212     *
     213     *     @type string $action       Type of action. Default 'update'.
     214     *     @type string $type         Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'.
     215     *     @type bool   $bulk         Whether the update process is a bulk update. Default true.
     216     *     @type array  $plugins      Array of the basename paths of the plugins' main files.
     217     *     @type array  $themes       The theme slugs.
     218     *     @type array  $translations {
     219     *         Array of translations update data.
     220     *
     221     *         @type string $language The locale the translation is for.
     222     *         @type string $type     Type of translation. Accepts 'plugin', 'theme', or 'core'.
     223     *         @type string $slug     Text domain the translation is for. The slug of a theme/plugin or
     224     *                                'default' for core translations.
     225     *         @type string $version  The version of a theme, plugin, or core.
     226     *     }
     227     * }
     228     * @return void
     229     */
     230    public function invalidate_mo_files_cache( $upgrader, $hook_extra ) {
     231        if ( 'translation' !== $hook_extra['type'] || array() === $hook_extra['translations'] ) {
     232            return;
     233        }
     234
     235        $translation_types = array_unique( wp_list_pluck( $hook_extra['translations'], 'type' ) );
     236
     237        foreach ( $translation_types as $type ) {
     238            switch ( $type ) {
     239                case 'plugin':
     240                    wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' );
     241                    break;
     242                case 'theme':
     243                    wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' );
     244                    break;
     245                default:
     246                    wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' );
     247                    break;
     248            }
     249        }
     250    }
     251
     252    /**
    138253     * Returns possible language directory paths for a given text domain.
    139254     *
     
    157272
    158273    /**
    159      * Gets the path to the language directory for the current locale.
     274     * Gets the path to the language directory for the current domain and locale.
    160275     *
    161276     * Checks the plugins and themes language directories as well as any
     
    176291
    177292        foreach ( $locations as $location ) {
    178             if ( ! isset( $this->cached_mo_files[ $location ] ) ) {
    179                 $this->set_cached_mo_files( $location );
    180             }
     293            $files = $this->get_language_files_from_path( $location );
    181294
    182295            $path = "$location/$domain-$locale.mo";
    183296
    184             foreach ( $this->cached_mo_files[ $location ] as $mo_path ) {
     297            foreach ( $files as $mo_path ) {
    185298                if (
    186299                    ! in_array( $domain, $this->domains_with_translations, true ) &&
     
    216329        return false;
    217330    }
    218 
    219     /**
    220      * Reads and caches all available MO files from a given directory.
    221      *
    222      * @since 6.1.0
    223      *
    224      * @param string $path Language directory path.
    225      */
    226     private function set_cached_mo_files( $path ) {
    227         $this->cached_mo_files[ $path ] = array();
    228 
    229         $mo_files = glob( $path . '/*.mo' );
    230 
    231         if ( $mo_files ) {
    232             $this->cached_mo_files[ $path ] = $mo_files;
    233         }
    234     }
    235331}
  • trunk/src/wp-includes/l10n.php

    r57286 r57287  
    13991399 * @since 4.7.0 The results are now filterable with the {@see 'get_available_languages'} filter.
    14001400 *
     1401 * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry.
     1402 *
    14011403 * @param string $dir A directory to search for language files.
    14021404 *                    Default WP_LANG_DIR.
     
    14051407 */
    14061408function get_available_languages( $dir = null ) {
     1409    global $wp_textdomain_registry;
     1410
    14071411    $languages = array();
    14081412
    1409     $lang_files = glob( ( is_null( $dir ) ? WP_LANG_DIR : $dir ) . '/*.mo' );
     1413    $path       = is_null( $dir ) ? WP_LANG_DIR : $dir;
     1414    $lang_files = $wp_textdomain_registry->get_language_files_from_path( $path );
     1415
    14101416    if ( $lang_files ) {
    14111417        foreach ( $lang_files as $lang_file ) {
  • trunk/src/wp-settings.php

    r57269 r57287  
    381381 */
    382382$GLOBALS['wp_textdomain_registry'] = new WP_Textdomain_Registry();
     383$GLOBALS['wp_textdomain_registry']->init();
    383384
    384385// Load multisite-specific files.
  • trunk/tests/phpunit/tests/l10n/wpTextdomainRegistry.php

    r55010 r57287  
    1919    }
    2020
     21    public function tear_down() {
     22        wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/foobar/' ), 'translations' );
     23        wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' );
     24        wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' );
     25        wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' );
     26
     27        parent::tear_down();
     28    }
     29
    2130    /**
    2231     * @covers ::has
     
    2534     */
    2635    public function test_set_custom_path() {
    27         $reflection          = new ReflectionClass( $this->instance );
    28         $reflection_property = $reflection->getProperty( 'cached_mo_files' );
    29         $reflection_property->setAccessible( true );
    30 
    31         $this->assertEmpty(
    32             $reflection_property->getValue( $this->instance ),
    33             'Cache not empty by default'
    34         );
    35 
    3636        $this->instance->set_custom_path( 'foo', WP_LANG_DIR . '/bar' );
    3737
     
    4949            'Custom path for textdomain not returned'
    5050        );
    51         $this->assertArrayHasKey(
    52             WP_LANG_DIR . '/bar',
    53             $reflection_property->getValue( $this->instance ),
    54             'Custom path missing from cache'
     51        $this->assertNotFalse(
     52            wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . 'bar/' ), 'translations' ),
     53            'List of files in custom path not cached'
    5554        );
    5655    }
     
    6160     */
    6261    public function test_get( $domain, $locale, $expected ) {
    63         $reflection          = new ReflectionClass( $this->instance );
    64         $reflection_property = $reflection->getProperty( 'cached_mo_files' );
    65         $reflection_property->setAccessible( true );
    66 
    6762        $actual = $this->instance->get( $domain, $locale );
    6863        $this->assertSame(
     
    7065            $actual,
    7166            'Expected languages directory path not matching actual one'
    72         );
    73 
    74         $this->assertArrayHasKey(
    75             WP_LANG_DIR . '/plugins',
    76             $reflection_property->getValue( $this->instance ),
    77             'Default plugins path missing from cache'
    7867        );
    7968    }
     
    9079            $this->instance->get( 'foo-plugin', 'de_DE' )
    9180        );
     81    }
     82
     83    /**
     84     * @covers ::get_language_files_from_path
     85     */
     86    public function test_get_language_files_from_path_caches_results() {
     87        $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/foobar/' );
     88        $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/plugins/' );
     89        $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/themes/' );
     90        $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) );
     91
     92        $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ) );
     93        $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' ) );
     94        $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/foobar/' ), 'translations' ) );
     95        $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' ) );
     96    }
     97
     98    /**
     99     * @covers ::get_language_files_from_path
     100     */
     101    public function test_get_language_files_from_path_short_circuit() {
     102        add_filter( 'pre_get_language_files_from_path', '__return_empty_array' );
     103        $result = $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/plugins/' );
     104        remove_filter( 'pre_get_language_files_from_path', '__return_empty_array' );
     105
     106        $cache = wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' );
     107
     108        $this->assertEmpty( $result );
     109        $this->assertFalse( $cache );
     110    }
     111
     112    /**
     113     * @covers ::invalidate_mo_files_cache
     114     */
     115    public function test_invalidate_mo_files_cache() {
     116        $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/plugins/' );
     117        $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/themes/' );
     118        $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) );
     119
     120        $this->instance->invalidate_mo_files_cache(
     121            null,
     122            array(
     123                'type'         => 'translation',
     124                'translations' => array(
     125                    (object) array(
     126                        'type'     => 'plugin',
     127                        'slug'     => 'internationalized-plugin',
     128                        'language' => 'de_DE',
     129                        'version'  => '99.9.9',
     130                    ),
     131                    (object) array(
     132                        'type'     => 'theme',
     133                        'slug'     => 'internationalized-theme',
     134                        'language' => 'de_DE',
     135                        'version'  => '99.9.9',
     136                    ),
     137                    (object) array(
     138                        'type'     => 'core',
     139                        'slug'     => 'default',
     140                        'language' => 'es_ES',
     141                        'version'  => '99.9.9',
     142                    ),
     143                ),
     144            )
     145        );
     146
     147        $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ) );
     148        $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' ) );
     149        $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' ) );
    92150    }
    93151
Note: See TracChangeset for help on using the changeset viewer.