From bba3827bedf30058c7091cb1869646705cb4a51b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 6 Nov 2023 21:58:37 +0100 Subject: [PATCH 01/21] Env: add preliminary object cache support --- .../env/lib/build-docker-compose-config.js | 30 +++++++ packages/env/lib/commands/start.js | 9 ++- .../get-config-from-environment-vars.js | 1 + packages/env/lib/config/parse-config.js | 22 ++++++ packages/env/lib/config/validate-config.js | 25 ++++++ packages/env/lib/init-config.js | 79 ++++++++++++++++--- 6 files changed, 152 insertions(+), 14 deletions(-) diff --git a/packages/env/lib/build-docker-compose-config.js b/packages/env/lib/build-docker-compose-config.js index 5336f8690cca8..46c7684bcd6a0 100644 --- a/packages/env/lib/build-docker-compose-config.js +++ b/packages/env/lib/build-docker-compose-config.js @@ -261,6 +261,36 @@ module.exports = function buildDockerComposeConfig( config ) { }, extra_hosts: [ 'host.docker.internal:host-gateway' ], }, + 'object-cache': { + depends_on: [ 'wordpress' ], + build: { + context: '.', + dockerfile: 'ObjectCache.Dockerfile', + args: imageBuildArgs, + }, + volumes: developmentMounts, + user: hostUser.fullUser, + environment: { + ...dbEnv.credentials, + ...dbEnv.development, + }, + extra_hosts: [ 'host.docker.internal:host-gateway' ], + }, + 'tests-object-cache': { + depends_on: [ 'tests-wordpress' ], + build: { + context: '.', + dockerfile: 'Tests-ObjectCache.Dockerfile', + args: imageBuildArgs, + }, + volumes: testsMounts, + user: hostUser.fullUser, + environment: { + ...dbEnv.credentials, + ...dbEnv.tests, + }, + extra_hosts: [ 'host.docker.internal:host-gateway' ], + }, }, volumes: { ...( ! config.env.development.coreSource && { wordpress: {} } ), diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index 2765e9c4e3198..1bac41c6c91a3 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -171,7 +171,14 @@ module.exports = async function start( { spinner.text = 'Starting WordPress.'; await dockerCompose.upMany( - [ 'wordpress', 'tests-wordpress', 'cli', 'tests-cli' ], + [ + 'wordpress', + 'tests-wordpress', + 'cli', + 'tests-cli', + 'object-cache', + 'tests-object-cache', + ], { ...dockerComposeConfig, commandOptions: shouldConfigureWp diff --git a/packages/env/lib/config/get-config-from-environment-vars.js b/packages/env/lib/config/get-config-from-environment-vars.js index 8a4608b859b7f..dfc60536d9a63 100644 --- a/packages/env/lib/config/get-config-from-environment-vars.js +++ b/packages/env/lib/config/get-config-from-environment-vars.js @@ -20,6 +20,7 @@ const { checkPort, checkVersion, checkString } = require( './validate-config' ); * @property {?number} testsPort An override for the testing environment's port. * @property {?WPSource} coreSource An override for all environment's coreSource. * @property {?string} phpVersion An override for all environment's PHP version. + * @property {?string} objectCache An override for all environment's object cache configuration. * @property {?Object.} lifecycleScripts An override for various lifecycle scripts. */ diff --git a/packages/env/lib/config/parse-config.js b/packages/env/lib/config/parse-config.js index 3f69bb78b81f2..4a39349226874 100644 --- a/packages/env/lib/config/parse-config.js +++ b/packages/env/lib/config/parse-config.js @@ -18,6 +18,7 @@ const { checkStringArray, checkObjectWithValues, checkVersion, + checkOneOfStrings, checkValidURL, } = require( './validate-config' ); const getConfigFromEnvironmentVars = require( './get-config-from-environment-vars' ); @@ -53,6 +54,7 @@ const mergeConfigs = require( './merge-configs' ); * @property {Object} config Mapping of wp-config.php constants to their desired values. * @property {Object.} mappings Mapping of WordPress directories to local directories which should be mounted. * @property {string|null} phpVersion Version of PHP to use in the environments, of the format 0.0. + * @property {string|null} objectCache Type of object cache to set up. Supports "memcached" or none (null). */ /** @@ -85,6 +87,7 @@ const DEFAULT_ENVIRONMENT_CONFIG = { themes: [], port: 8888, testsPort: 8889, + objectCache: null, mappings: {}, config: { FS_METHOD: 'direct', @@ -293,6 +296,12 @@ function getEnvironmentVarOverrides( cacheDirectoryPath ) { overrideConfig.env.tests.phpVersion = overrides.phpVersion; } + if ( overrides.objectCache ) { + overrideConfig.objectCache = overrides.objectCache; + overrideConfig.env.development.objectCache = overrides.objectCache; + overrideConfig.env.tests.objectCache = overrides.objectCache; + } + return overrideConfig; } @@ -455,6 +464,19 @@ async function parseEnvironmentConfig( ); } + if ( config.objectCache !== undefined ) { + // Support null as a valid input. + if ( config.objectCache !== null ) { + checkOneOfStrings( + configFile, + `${ environmentPrefix }objectCache`, + config.objectCache, + [ 'memcached' ] + ); + } + parsedConfig.objectCache = config.objectCache; + } + if ( config.plugins !== undefined ) { checkStringArray( configFile, diff --git a/packages/env/lib/config/validate-config.js b/packages/env/lib/config/validate-config.js index 4aa62cb457155..440ac28b1d05f 100644 --- a/packages/env/lib/config/validate-config.js +++ b/packages/env/lib/config/validate-config.js @@ -25,6 +25,30 @@ function checkString( configFile, configKey, value ) { } } +/** + * Validates the string is one of the supported options. + * + * @param {string} configFile The config file we're validating. + * @param {string} configKey The configuration key we're validating. + * @param {string} value The value to check. + * @param {string[]} options The supported options. + */ +function checkOneOfStrings( configFile, configKey, value, options ) { + if ( typeof version !== 'string' ) { + throw new ValidationError( + `Invalid ${ configFile }: "${ configKey }" must be a string.` + ); + } + + if ( ! options.includes( value ) ) { + throw new ValidationError( + `Invalid ${ configFile }: "${ configKey }" must be one of ${ options.join( + ',' + ) }` + ); + } +} + /** * Validates the port and throws if it isn't valid. * @@ -173,6 +197,7 @@ function checkValidURL( configFile, configKey, url ) { module.exports = { ValidationError, checkString, + checkOneOfStrings, checkPort, checkStringArray, checkObjectWithValues, diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index 318bcae151b29..771749211ff79 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -81,20 +81,41 @@ module.exports = async function initConfig( { ); // Write four Dockerfiles for each service we provided. - // (WordPress and CLI services, then a development and test environment for each.) - for ( const imageType of [ 'WordPress', 'CLI' ] ) { + // (WordPress, CLI, and Object Cache services, then a development and test environment for each.) + for ( const imageType of [ 'WordPress', 'CLI', 'ObjectCache' ] ) { for ( const envType of [ 'development', 'tests' ] ) { - await writeFile( - path.resolve( - config.workDirectoryPath, - `${ - envType === 'tests' ? 'Tests-' : '' - }${ imageType }.Dockerfile` - ), - imageType === 'WordPress' - ? wordpressDockerFileContents( envType, config ) - : cliDockerFileContents( envType, config ) - ); + let fileContents = ''; + switch ( imageType ) { + case 'WordPress': + fileContents = wordpressDockerFileContents( + envType, + config + ); + break; + case 'CLI': + fileContents = cliDockerFileContents( envType, config ); + break; + case 'ObjectCache': + fileContents = objectCacheDockerFileContents( + envType, + config + ); + break; + default: + break; + } + + if ( fileContents ) { + await writeFile( + path.resolve( + config.workDirectoryPath, + `${ + envType === 'tests' ? 'Tests-' : '' + }${ imageType }.Dockerfile` + ), + fileContents + ); + } } } } else if ( ! existsSync( config.workDirectoryPath ) ) { @@ -177,6 +198,24 @@ CMD [ "/bin/sh", "-c", "while true; do sleep 2073600; done" ] `; } +/** + * Generates the Dockerfile used by wp-env's `object-cache` and `tests-object-cache` instances. + * + * @param {string} env The environment we're installing -- development or tests. + * @param {WPConfig} config The configuration object. + * @return {string} The dockerfile contents. + */ +function objectCacheDockerFileContents( env, config ) { + if ( 'memcached' === config.env[ env ].objectCache ) { + return `FROM memcached + +CMD ["memcached"] + `; + } + + return ''; +} + /** * Generates content for the Dockerfile to install dependencies. * @@ -207,6 +246,11 @@ RUN apt-get -qy install git # Set up sudo so they can have root access. RUN apt-get -qy install sudo RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; + + if ( 'memcached' === config.env[ env ].objectCache ) { + dockerFileContent += 'RUN apt-get -qy install libmemcached-dev'; + } + break; } case 'cli': { @@ -220,6 +264,11 @@ RUN apk --no-cache add $PHPIZE_DEPS && touch /usr/local/etc/php/php.ini # Set up sudo so they can have root access. RUN apk --no-cache add sudo linux-headers RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; + + if ( 'memcached' === config.env[ env ].objectCache ) { + dockerFileContent += 'RUN apk add libmemcached-dev'; + } + break; } default: { @@ -232,6 +281,10 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; config.env[ env ].phpVersion ); + if ( 'memcached' === config.env[ env ].objectCache ) { + dockerFileContent += 'RUN pecl install memcached-3.1.5'; + } + // Add better PHP settings. dockerFileContent += ` RUN echo 'upload_max_filesize = 1G' >> /usr/local/etc/php/php.ini From 87a1ea5bc7b8df3ec76046b3462681bc18d8b941 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 6 Nov 2023 21:59:48 +0100 Subject: [PATCH 02/21] Update wp-env config for debugging --- .wp-env.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.wp-env.json b/.wp-env.json index 79810b194b667..6fccb9017ad0c 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -2,6 +2,7 @@ "core": "WordPress/WordPress", "plugins": [ "." ], "themes": [ "./test/emptytheme" ], + "objectCache": "memcached", "env": { "tests": { "mappings": { From 64931e1807cd803865f44eced8769924df35b798 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 6 Nov 2023 22:13:05 +0100 Subject: [PATCH 03/21] copyFile --- packages/env/lib/commands/start.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index 1bac41c6c91a3..690e6d076c2d8 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -166,6 +166,29 @@ module.exports = async function start( { spinner, debug ); + + if ( 'memcached' === config.env.development.objectCache ) { + // Set up object cache drop-in if configured. + const memcachedFile = require.resolve( + '@wordpress/env/lib/object-cache/memcached.php', + { + paths: [ process.cwd(), __dirname ], + } + ); + + fs.copyFile( + memcachedFile, + path.join( + config.env.development.coreSource.path, + 'wp-content' + ) + ); + + fs.copyFile( + memcachedFile, + path.join( config.env.tests.coreSource.path, 'wp-content' ) + ); + } } spinner.text = 'Starting WordPress.'; From 476f97a67f5d95c46933a175b9913a1e0e3e54e9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 6 Nov 2023 22:13:33 +0100 Subject: [PATCH 04/21] Fix var --- packages/env/lib/config/validate-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/env/lib/config/validate-config.js b/packages/env/lib/config/validate-config.js index 440ac28b1d05f..d80575dcab6e2 100644 --- a/packages/env/lib/config/validate-config.js +++ b/packages/env/lib/config/validate-config.js @@ -34,7 +34,7 @@ function checkString( configFile, configKey, value ) { * @param {string[]} options The supported options. */ function checkOneOfStrings( configFile, configKey, value, options ) { - if ( typeof version !== 'string' ) { + if ( typeof value !== 'string' ) { throw new ValidationError( `Invalid ${ configFile }: "${ configKey }" must be a string.` ); From 3e1bbe0563f41b0c8ad13bca6fc95b9dd8384ab5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 6 Nov 2023 22:23:11 +0100 Subject: [PATCH 05/21] Try `path.resolve` --- packages/env/lib/commands/start.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index 690e6d076c2d8..b08cbab9bda4b 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -169,12 +169,7 @@ module.exports = async function start( { if ( 'memcached' === config.env.development.objectCache ) { // Set up object cache drop-in if configured. - const memcachedFile = require.resolve( - '@wordpress/env/lib/object-cache/memcached.php', - { - paths: [ process.cwd(), __dirname ], - } - ); + const memcachedFile = path.resolve( __dirname, '../memcached.php' ); fs.copyFile( memcachedFile, From 62c064a0f0f420c89c7e7c0cb5592d4027d497bf Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 6 Nov 2023 22:28:50 +0100 Subject: [PATCH 06/21] Add missing file --- packages/env/lib/object-cache/memcached.php | 2385 +++++++++++++++++++ 1 file changed, 2385 insertions(+) create mode 100644 packages/env/lib/object-cache/memcached.php diff --git a/packages/env/lib/object-cache/memcached.php b/packages/env/lib/object-cache/memcached.php new file mode 100644 index 0000000000000..ef03546892037 --- /dev/null +++ b/packages/env/lib/object-cache/memcached.php @@ -0,0 +1,2385 @@ +add( $key, $value, $group, $expiration ); +} + +/** + * Adds a value to cache on a specific server. + * + * Using a server_key value, the object can be stored on a specified server as opposed + * to a random server in the stack. Note that this method will add the key/value to the + * _cache object as part of the runtime cache. It will add it to an array for the + * specified server_key. + * + * @link https://www.php.net/manual/en/memcached.addbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ +function wp_cache_add_by_key( $server_key, $key, $value, $group = '', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->addByKey( $server_key, $key, $value, $group, $expiration ); +} + +/** + * Adds multiple values to the cache in one call, if the cache keys don't already exist. + * + * @param array $items Array of keys and values to be added. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param int $expiration Optional. When to expire the cache contents, in seconds. + * Default 0 (no expiration). + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false if cache key and group already exist. + */ +function wp_cache_add_multiple( array $items, $group = '', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->addMultiple( $items, $group, $expiration ); +} + +/** + * Adds a single server to the list of Memcached servers. + * + * @link https://www.php.net/manual/en/memcached.addserver.php + * + * @param string $host The hostname of the memcache server. + * @param int $port The port on which memcache is running. + * @param int $weight The weight of the server relative to the total weight + * of all the servers in the pool. + * @return bool True on success, false on failure. + */ +function wp_cache_add_server( $host, $port, $weight = 0 ) { + global $wp_object_cache; + return $wp_object_cache->addServer( $host, $port, $weight ); +} + +/** + * Adds an array of servers to the pool. + * + * Each individual server in the array must include a domain and port, with an optional + * weight value: $servers = array( array( '127.0.0.1', 11211, 0 ) ); + * + * @link https://www.php.net/manual/en/memcached.addservers.php + * + * @param array $servers Array of server to register. + * @return bool True on success, false on failure. + */ +function wp_cache_add_servers( $servers ) { + global $wp_object_cache; + return $wp_object_cache->addServers( $servers ); +} + +/** + * Appends data to an existing item. + * + * This method should throw an error if it is used with compressed data. + * This is an expected behavior. Memcached casts the value to be appended to the initial value + * to the type of the initial value. Be careful as this leads to unexpected behavior at times. + * Due to how memcached treats types, the behavior has been mimicked in the internal cache to produce + * similar results and improve consistency. It is recommended that appends only occur with data of + * the same type. + * + * @link https://www.php.net/manual/en/memcached.append.php + * + * @param string $key The key under which to store the value. + * @param mixed $value Must be string as appending mixed values is not well-defined. + * @param string $group The group value appended to the $key. + * @return bool True on success, false on failure. + */ +function wp_cache_append( $key, $value, $group = '' ) { + global $wp_object_cache; + return $wp_object_cache->append( $key, $value, $group ); +} + +/** + * Appends data to an existing item by server key. + * + * This method should throw an error if it is used with compressed data. + * This is an expected behavior. Memcached casts the value to be appended to the initial value + * to the type of the initial value. Be careful as this leads to unexpected behavior at times. + * Due to how memcached treats types, the behavior has been mimicked in the internal cache to produce + * similar results and improve consistency. It is recommended that appends only occur with data of + * the same type. + * + * @link https://www.php.net/manual/en/memcached.appendbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param mixed $value Must be string as appending mixed values is not well-defined. + * @param string $group The group value appended to the $key. + * @return bool True on success, false on failure. + */ +function wp_cache_append_by_key( $server_key, $key, $value, $group = '' ) { + global $wp_object_cache; + return $wp_object_cache->appendByKey( $server_key, $key, $value, $group ); +} + +/** + * Performs a "check and set" to store data. + * + * The set will be successful only if the no other request has updated the value + * since it was fetched by this request. + * + * @link https://www.php.net/manual/en/memcached.cas.php + * + * @param float $cas_token Unique value associated with the existing item. Generated by memcached. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ +function wp_cache_cas( $cas_token, $key, $value, $group = '', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->cas( $cas_token, $key, $value, $group, $expiration ); +} + +/** + * Performs a "check and set" to store data with a server key. + * + * The set will be successful only if the no other request has updated the value + * since it was fetched by this request. + * + * @link https://www.php.net/manual/en/memcached.casbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param float $cas_token Unique value associated with the existing item. Generated by memcached. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ +function wp_cache_cas_by_key( $cas_token, $server_key, $key, $value, $group = '', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->casByKey( $cas_token, $server_key, $key, $value, $group, $expiration ); +} + +/** + * Closes the cache. + * + * This function has ceased to do anything since WordPress 2.5. + * The functionality was removed along with the rest of the persistent cache. + * This does not mean that plugins can't implement this function when they need + * to make sure that the cache is cleaned up after WordPress no longer needs it. + * + * @since 2.0.0 + * + * @return bool Always returns true. + */ +function wp_cache_close() { + return true; +} + +/** + * Decrements a numeric item's value. + * + * @link https://www.php.net/manual/en/memcached.decrement.php + * + * @param string $key The key under which to store the value. + * @param int $offset The amount by which to decrement the item's value. + * @param string $group The group value appended to the $key. + * @return int|bool Item's new value on success, false on failure. + */ +function wp_cache_decrement( $key, $offset = 1, $group = '' ) { + global $wp_object_cache; + return $wp_object_cache->decrement( $key, $offset, $group ); +} + +/** + * Decrements a numeric item's value. + * + * This is the same as wp_cache_decrement(), but kept for backward compatibility. + * The original WordPress caching backends use wp_cache_decr(). + * + * @link https://www.php.net/manual/en/memcached.decrement.php + * + * @param string $key The key under which to store the value. + * @param int $offset The amount by which to decrement the item's value. + * @param string $group The group value appended to the $key. + * @return int|bool Item's new value on success, false on failure. + */ +function wp_cache_decr( $key, $offset = 1, $group = '' ) { + return wp_cache_decrement( $key, $offset, $group ); +} + +/** + * Removes the item from the cache. + * + * Removes an item from memcached with identified by $key after $time seconds. + * The $time parameter allows an object to be queued for deletion without + * immediately deleting. Between the time that it is queued and the time it's deleted, + * add, replace, and get will fail, but set will succeed. + * + * @link https://www.php.net/manual/en/memcached.delete.php + * + * @param string $key The key under which to store the value. + * @param string $group The group value appended to the $key. + * @param int $time The amount of time the server will wait to delete the item in seconds. + * @return bool True on success, false on failure. + */ +function wp_cache_delete( $key, $group = '', $time = 0 ) { + global $wp_object_cache; + return $wp_object_cache->delete( $key, $group, $time ); +} + +/** + * Removes the item from the cache by server key. + * + * Removes an item from memcached with identified by $key after $time seconds. + * The $time parameter allows an object to be queued for deletion without + * immediately deleting. Between the time that it is queued and the time it's deleted, + * add, replace, and get will fail, but set will succeed. + * + * @link https://www.php.net/manual/en/memcached.deletebykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param string $group The group value appended to the $key. + * @param int $time The amount of time the server will wait to delete the item in seconds. + * @return bool True on success, false on failure. + */ +function wp_cache_delete_by_key( $server_key, $key, $group = '', $time = 0 ) { + global $wp_object_cache; + return $wp_object_cache->deleteByKey( $server_key, $key, $group, $time ); +} + +/** + * Deletes multiple values from the cache in one call. + * + * @param array $keys Array of keys under which the cache to deleted. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false if the contents were not deleted. + */ +function wp_cache_delete_multiple( array $keys, $group = '' ) { + global $wp_object_cache; + return $wp_object_cache->deleteMultiple( $keys, $group ); +} + +/** + * Fetches the next result. + * + * @link https://www.php.net/manual/en/memcached.fetch.php + * + * @return array|false The next result on success, false on failure. + */ +function wp_cache_fetch() { + global $wp_object_cache; + return $wp_object_cache->fetch(); +} + +/** + * Fetches all remaining results from the last request. + * + * @link https://www.php.net/manual/en/memcached.fetchall.php + * + * @return array|false The results on success, false on failure. + */ +function wp_cache_fetch_all() { + global $wp_object_cache; + return $wp_object_cache->fetchAll(); +} + +/** + * Invalidates all items in the cache. + * + * @link https://www.php.net/manual/en/memcached.flush.php + * + * @param int $delay Number of seconds to wait before invalidating the items. + * @return bool True on success, false on failure. + */ +function wp_cache_flush( $delay = 0 ) { + global $wp_object_cache; + return $wp_object_cache->flush( $delay ); +} + +/** + * Removes all cache items from the in-memory runtime cache. + * + * @return bool True on success, false on failure. + */ +function wp_cache_flush_runtime() { + global $wp_object_cache; + return $wp_object_cache->flush_runtime(); +} + +/** + * Determines whether the object cache implementation supports a particular feature. + * + * @since 6.1.0 + * + * @param string $feature Name of the feature to check for. Possible values include: + * 'add_multiple', 'set_multiple', 'get_multiple', 'delete_multiple', + * 'flush_runtime', 'flush_group'. + * @return bool True if the feature is supported, false otherwise. + */ +function wp_cache_supports( $feature ) { + switch ( $feature ) { + case 'get_multiple': + case 'flush_runtime': + return true; + default: + return false; + } +} + +/** + * Retrieves object from cache. + * + * Gets an object from cache based on $key and $group. In order to fully support + * the $cache_cb and $cas_token parameters, the runtime cache is ignored by this function + * if either of those values are set. In that case, the request is made directly + * to the memcached server for proper handling of the callback and/or token. + * + * Note that the $deprecated and $found args are only here for compatibility + * with the native wp_cache_get() function. + * + * @link https://www.php.net/manual/en/memcached.get.php + * + * @param string $key The key under which to store the value. + * @param string $group The group value appended to the $key. + * @param bool $force Whether or not to force a cache invalidation. + * @param null|bool $found Variable passed by reference to determine if the value was found or not. + * @param null|string $cache_cb Read-through caching callback. + * @param null|float $cas_token The variable to store the CAS token in. + * @return bool|mixed Cached object value. + */ +function wp_cache_get( $key, $group = '', $force = false, &$found = null, $cache_cb = null, &$cas_token = null ) { + global $wp_object_cache; + + if ( func_num_args() > 4 ) { + return $wp_object_cache->get( $key, $group, $force, $found, '', false, $cache_cb, $cas_token ); + } else { + return $wp_object_cache->get( $key, $group, $force, $found ); + } +} + +/** + * Retrieves object from cache from specified server. + * + * Gets an object from cache based on $key, $group, and $server_key. In order to fully support + * the $cache_cb and $cas_token parameters, the runtime cache is ignored by this function + * if either of those values are set. In that case, the request is made directly + * to the memcached server for proper handling of the callback and/or token. + * + * @link https://www.php.net/manual/en/memcached.getbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param string $group The group value appended to the $key. + * @param bool $force Whether or not to force a cache invalidation. + * @param null|bool $found Variable passed by reference to determine if the value was found or not. + * @param null|string $cache_cb Read-through caching callback. + * @param null|float $cas_token The variable to store the CAS token in. + * @return bool|mixed Cached object value. + */ +function wp_cache_get_by_key( $server_key, $key, $group = '', $force = false, &$found = null, $cache_cb = null, &$cas_token = null ) { + global $wp_object_cache; + + if ( func_num_args() > 5 ) { + return $wp_object_cache->getByKey( $server_key, $key, $group, $force, $found, $cache_cb, $cas_token ); + } else { + return $wp_object_cache->getByKey( $server_key, $key, $group, $force, $found ); + } +} + +/** + * Requests multiple keys without blocking. + * + * @link https://www.php.net/manual/en/memcached.getdelayed.php + * + * @param string|array $keys Array or string of key(s) to request. + * @param string|array $groups Array or string of group(s) for the key(s). + * See buildKeys for more on how these are handled. + * @param bool $with_cas Whether to request CAS token values also. + * @param null $value_cb The result callback or null. + * @return bool True on success, false on failure. + */ +function wp_cache_get_delayed( $keys, $groups = '', $with_cas = false, $value_cb = null ) { + global $wp_object_cache; + return $wp_object_cache->getDelayed( $keys, $groups, $with_cas, $value_cb ); +} + +/** + * Requests multiple keys without blocking from a specified server. + * + * @link https://www.php.net/manual/en/memcached.getdelayed.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string|array $keys Array or string of key(s) to request. + * @param string|array $groups Array or string of group(s) for the key(s). + * See buildKeys for more on how these are handled. + * @param bool $with_cas Whether to request CAS token values also. + * @param null $value_cb The result callback or null. + * @return bool True on success, false on failure. + */ +function wp_cache_get_delayed_by_key( $server_key, $keys, $groups = '', $with_cas = false, $value_cb = null ) { + global $wp_object_cache; + return $wp_object_cache->getDelayedByKey( $server_key, $keys, $groups, $with_cas, $value_cb ); +} + +/** + * Gets multiple values from memcached in one request. + * + * See the buildKeys method definition to understand the $keys/$groups parameters. + * + * @link https://www.php.net/manual/en/memcached.getmulti.php + * + * @param array $keys Array of keys to retrieve. + * @param string|array $groups If string, used for all keys. + * If arrays, corresponds with the $keys array. + * @param null|array $cas_tokens The variable to store the CAS tokens for the found items. + * @param int $flags The flags for the get operation. + * @return bool|array The array of found items on success, false on failure. + */ +function wp_cache_get_multi( $keys, $groups = '', &$cas_tokens = null, $flags = null ) { + global $wp_object_cache; + + if ( func_num_args() > 2 ) { + return $wp_object_cache->getMulti( $keys, $groups, '', $cas_tokens, $flags ); + } else { + return $wp_object_cache->getMulti( $keys, $groups ); + } +} + +/** + * Gets multiple values from memcached in one request by specified server key. + * + * See the buildKeys method definition to understand the $keys/$groups parameters. + * + * @link https://www.php.net/manual/en/memcached.getmultibykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param array $keys Array of keys to retrieve. + * @param string|array $groups If string, used for all keys. + * If arrays, corresponds with the $keys array. + * @param null|array $cas_tokens The variable to store the CAS tokens for the found items. + * @param int $flags The flags for the get operation. + * @return bool|array The array of found items on success, false on failure. + */ +function wp_cache_get_multi_by_key( $server_key, $keys, $groups = '', &$cas_tokens = null, $flags = null ) { + global $wp_object_cache; + + if ( func_num_args() > 3 ) { + return $wp_object_cache->getMultiByKey( $server_key, $keys, $groups, $cas_tokens, $flags ); + } else { + return $wp_object_cache->getMultiByKey( $server_key, $keys, $groups ); + } +} + +/** + * Retrieves multiple values from the cache in one call. + * + * @param array $keys Array of keys under which the cache contents are stored. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param bool $force Optional. Whether to force an update of the local cache + * from the persistent cache. Default false. + * @return array Array of return values, grouped by key. Each value is either + * the cache contents on success, or false on failure. + */ +function wp_cache_get_multiple( $keys, $group = '', $force = false ) { + global $wp_object_cache; + + // Prime multiple keys in a single Memcached call. + $wp_object_cache->getMulti( $keys, $group ); + + return $wp_object_cache->getMultiple( $keys, $group, $force ); +} + +/** + * Retrieves a Memcached option value. + * + * @link https://www.php.net/manual/en/memcached.getoption.php + * + * @param int $option One of the Memcached::OPT_* constants. + * @return mixed The value of the requested option on success, false on failure. + */ +function wp_cache_get_option( $option ) { + global $wp_object_cache; + return $wp_object_cache->getOption( $option ); +} + +/** + * Returns the result code of the last option. + * + * @link https://www.php.net/manual/en/memcached.getresultcode.php + * + * @return int Result code of the last Memcached operation. + */ +function wp_cache_get_result_code() { + global $wp_object_cache; + return $wp_object_cache->getResultCode(); +} + +/** + * Return the message describing the result of the last operation. + * + * @link https://www.php.net/manual/en/memcached.getresultmessage.php + * + * @return string Message describing the result of the last Memcached operation. + */ +function wp_cache_get_result_message() { + global $wp_object_cache; + return $wp_object_cache->getResultMessage(); +} + +/** + * Gets server information by key. + * + * @link https://www.php.net/manual/en/memcached.getserverbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @return array Array with host, post, and weight on success, fales on failure. + */ +function wp_cache_get_server_by_key( $server_key ) { + global $wp_object_cache; + return $wp_object_cache->getServerByKey( $server_key ); +} + +/** + * Gets the list of servers in the pool. + * + * @link https://www.php.net/manual/en/memcached.getserverlist.php + * + * @return array The list of all servers in the server pool. + */ +function wp_cache_get_server_list() { + global $wp_object_cache; + return $wp_object_cache->getServerList(); +} + +/** + * Gets server pool statistics. + * + * @link https://www.php.net/manual/en/memcached.getstats.php + * + * @return array Array of server statistics, one entry per server. + */ +function wp_cache_get_stats() { + global $wp_object_cache; + return $wp_object_cache->getStats(); +} + +/** + * Gets server pool memcached version information. + * + * @link https://www.php.net/manual/en/memcached.getversion.php + * + * @return array Array of server versions, one entry per server. + */ +function wp_cache_get_version() { + global $wp_object_cache; + return $wp_object_cache->getVersion(); +} + +/** + * Increments a numeric item's value. + * + * @link https://www.php.net/manual/en/memcached.increment.php + * + * @param string $key The key under which to store the value. + * @param int $offset The amount by which to increment the item's value. + * @param string $group The group value appended to the $key. + * @return int|bool Item's new value on success, false on failure. + */ +function wp_cache_increment( $key, $offset = 1, $group = '' ) { + global $wp_object_cache; + return $wp_object_cache->increment( $key, $offset, $group ); +} + +/** + * Increments a numeric item's value. + * + * This is the same as wp_cache_increment(), but kept for backward compatibility. + * The original WordPress caching backends use wp_cache_incr(). + * + * @link https://www.php.net/manual/en/memcached.increment.php + * + * @param string $key The key under which to store the value. + * @param int $offset The amount by which to increment the item's value. + * @param string $group The group value appended to the $key. + * @return int|bool Item's new value on success, false on failure. + */ +function wp_cache_incr( $key, $offset = 1, $group = '' ) { + return wp_cache_increment( $key, $offset, $group ); +} + +/** + * Prepends data to an existing item. + * + * This method should throw an error if it is used with compressed data. This is an expected behavior. + * Memcached casts the value to be prepended to the initial value to the type of the initial value. + * Be careful as this leads to unexpected behavior at times. For instance, prepending (float) 45.23 + * to (int) 23 will result in 45, because the value is first combined (45.2323) then cast to "integer" + * (the original value), which will be (int) 45. Due to how memcached treats types, the behavior has been + * mimicked in the internal cache to produce similar results and improve consistency. It is recommended + * that prepends only occur with data of the same type. + * + * @link https://www.php.net/manual/en/memcached.prepend.php + * + * @param string $key The key under which to store the value. + * @param string $value Must be string as prepending mixed values is not well-defined. + * @param string $group The group value prepended to the $key. + * @return bool True on success, false on failure. + */ +function wp_cache_prepend( $key, $value, $group = '' ) { + global $wp_object_cache; + return $wp_object_cache->prepend( $key, $value, $group ); +} + +/** + * Appends data to an existing item by server key. + * + * This method should throw an error if it is used with compressed data. This is an expected behavior. + * Memcached casts the value to be prepended to the initial value to the type of the initial value. + * Be careful as this leads to unexpected behavior at times. For instance, prepending (float) 45.23 + * to (int) 23 will result in 45, because the value is first combined (45.2323) then cast to "integer" + * (the original value), which will be (int) 45. Due to how memcached treats types, the behavior has been + * mimicked in the internal cache to produce similar results and improve consistency. It is recommended + * that prepends only occur with data of the same type. + * + * @link https://www.php.net/manual/en/memcached.prependbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param string $value Must be string as prepending mixed values is not well-defined. + * @param string $group The group value prepended to the $key. + * @return bool True on success, false on failure. + */ +function wp_cache_prepend_by_key( $server_key, $key, $value, $group = '' ) { + global $wp_object_cache; + return $wp_object_cache->prependByKey( $server_key, $key, $value, $group ); +} + +/** + * Replaces a value in cache. + * + * This method is similar to "add"; however, is does not successfully set a value + * if the object's key is not already set in cache. + * + * @link https://www.php.net/manual/en/memcached.replace.php + * + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ +function wp_cache_replace( $key, $value, $group = '', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->replace( $key, $value, $group, $expiration ); +} + +/** + * Replaces a value in cache on a specific server. + * + * This method is similar to "addByKey"; however, is does not successfully set a value + * if the object's key is not already set in cache. + * + * @link https://www.php.net/manual/en/memcached.addbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ +function wp_cache_replace_by_key( $server_key, $key, $value, $group = '', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->replaceByKey( $server_key, $key, $value, $group, $expiration ); +} + +/** + * Sets a value in cache. + * + * The value is set whether or not this key already exists in memcached. + * + * @link https://www.php.net/manual/en/memcached.set.php + * + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ +function wp_cache_set( $key, $value, $group = '', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->set( $key, $value, $group, $expiration ); +} + +/** + * Sets a value in cache. + * + * The value is set whether or not this key already exists in memcached. + * + * @link https://www.php.net/manual/en/memcached.set.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ +function wp_cache_set_by_key( $server_key, $key, $value, $group = '', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->setByKey( $server_key, $key, $value, $group, $expiration ); +} + +/** + * Sets multiple values to cache at once. + * + * By sending an array of $items to this function, all values are saved at once to + * memcached, reducing the need for multiple requests to memcached. The $items array + * keys and values are what are stored to memcached. The keys in the $items array + * are merged with the $groups array/string value via buildKeys to determine the + * final key for the object. + * + * @param array $items An array of key/value pairs to store on the server. + * @param string|array $groups Group(s) to merge with key(s) in $items. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ +function wp_cache_set_multi( $items, $groups = '', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->setMulti( $items, $groups, $expiration ); +} + +/** + * Sets multiple values to cache at once on specified server. + * + * By sending an array of $items to this function, all values are saved at once to + * memcached, reducing the need for multiple requests to memcached. The $items array + * keys and values are what are stored to memcached. The keys in the $items array + * are merged with the $groups array/string value via buildKeys to determine the + * final key for the object. + * + * @param string $server_key The key identifying the server to store the value on. + * @param array $items An array of key/value pairs to store on the server. + * @param string|array $groups Group(s) to merge with key(s) in $items. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ +function wp_cache_set_multi_by_key( $server_key, $items, $groups = 'default', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->setMultiByKey( $server_key, $items, $groups, $expiration ); +} + +/** + * Sets multiple values to the cache in one call. + * + * Differs from wp_cache_add_multiple() in that it will always write data. + * + * @param array $items Array of keys and values to be set. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param int $expiration Optional. When to expire the cache contents, in seconds. + * Default 0 (no expiration). + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false on failure. + */ +function wp_cache_set_multiple( array $items, $group = '', $expiration = 0 ) { + global $wp_object_cache; + return $wp_object_cache->setMultiple( $items, $group, $expiration ); +} + +/** + * Sets a Memcached option. + * + * @link https://www.php.net/manual/en/memcached.setoption.php + * + * @param int $option Option name. + * @param mixed $value Option value. + * @return bool True on success, false on failure. + */ +function wp_cache_set_option( $option, $value ) { + global $wp_object_cache; + return $wp_object_cache->setOption( $option, $value ); +} + +/** + * Switches blog prefix, which changes the cache that is accessed. + * + * @param int $blog_id Blog to switch to. + */ +function wp_cache_switch_to_blog( $blog_id ) { + global $wp_object_cache; + return $wp_object_cache->switch_to_blog( $blog_id ); +} + + +/** + * Sets up Object Cache Global and assigns it. + * + * @global WP_Object_Cache $wp_object_cache WordPress Object Cache + */ +function wp_cache_init() { + global $wp_object_cache; + $wp_object_cache = new WP_Object_Cache(); +} + +/** + * Adds a group or set of groups to the list of non-persistent groups. + * + * @param string|array $groups A group or an array of groups to add. + */ +function wp_cache_add_global_groups( $groups ) { + global $wp_object_cache; + $wp_object_cache->add_global_groups( $groups ); +} + +/** + * Adds a group or set of groups to the list of non-Memcached groups. + * + * @param string|array $groups A group or an array of groups to add. + */ +function wp_cache_add_non_persistent_groups( $groups ) { + global $wp_object_cache; + $wp_object_cache->add_non_persistent_groups( $groups ); +} + +// phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid +class WP_Object_Cache { + + /** + * Holds the Memcached object. + * + * @var Memcached + */ + public $m; + + /** + * Hold the Memcached server details. + * + * @var array + */ + public $servers; + + /** + * Holds the non-Memcached objects. + * + * @var array + */ + public $cache = array(); + + /** + * List of global groups. + * + * @var array + */ + public $global_groups = array(); + + /** + * List of groups not saved to Memcached. + * + * @var array + */ + public $no_mc_groups = array(); + + /** + * Prefix used for global groups. + * + * @var string + */ + public $global_prefix = ''; + + /** + * Prefix used for non-global groups. + * + * @var string + */ + public $blog_prefix = ''; + + /** + * Thirty days in seconds. + * + * @var int + */ + public $thirty_days; + + /** + * Current unix time stamp. + * + * @var int + */ + public $now; + + /** + * Instantiates the Memcached class. + * + * Instantiates the Memcached class and returns adds the servers specified + * in the $memcached_servers global array. + * + * @link https://www.php.net/manual/en/memcached.construct.php + * + * @param null $persistent_id To create an instance that persists between requests, + * use persistent_id to specify a unique ID for the instance. + */ + public function __construct( $persistent_id = null ) { + global $memcached_servers, $blog_id, $table_prefix; + + if ( is_null( $persistent_id ) || ! is_string( $persistent_id ) ) { + $this->m = new Memcached(); + } else { + $this->m = new Memcached( $persistent_id ); + } + + if ( isset( $memcached_servers ) ) { + $this->servers = $memcached_servers; + } else { + $this->servers = array( array( 'memcached', 11211 ) ); + } + + $this->addServers( $this->servers ); + + /** + * This approach is borrowed from Sivel and Boren. Use the salt for easy cache invalidation + * and for multi single WP installations on the same server. + */ + if ( ! defined( 'WP_CACHE_KEY_SALT' ) ) { + define( 'WP_CACHE_KEY_SALT', '' ); + } + + // Assign global and blog prefixes for use with keys. + if ( function_exists( 'is_multisite' ) ) { + $this->global_prefix = ( is_multisite() || defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix; + $this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix ) . ':'; + } + + // Setup cacheable values for handling expiration times. + $this->thirty_days = 60 * 60 * 24 * 30; + $this->now = time(); + } + + /** + * Adds a value to cache. + * + * If the specified key already exists, the value is not stored and the function + * returns false. + * + * @link https://www.php.net/manual/en/memcached.add.php + * + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @param string $server_key The key identifying the server to store the value on. + * @param bool $by_key True to store in internal cache by key; false to not store by key. + * @return bool True on success, false on failure. + */ + public function add( $key, $value, $group = 'default', $expiration = 0, $server_key = '', $by_key = false ) { + /* + * Ensuring that wp_suspend_cache_addition is defined before calling, because sometimes an advanced-cache.php + * file will load object-cache.php before wp-includes/functions.php is loaded. In those cases, if wp_cache_add + * is called in advanced-cache.php before any more of WordPress is loaded, we get a fatal error because + * wp_suspend_cache_addition will not be defined until wp-includes/functions.php is loaded. + */ + if ( function_exists( 'wp_suspend_cache_addition' ) && wp_suspend_cache_addition() ) { + return false; + } + + $derived_key = $this->buildKey( $key, $group ); + + // Add does not set the value if the key exists; mimic that here. + if ( isset( $this->cache[ $derived_key ] ) ) { + return false; + } + + // If group is a non-Memcached group, save to runtime cache, not Memcached. + if ( in_array( $group, $this->no_mc_groups, true ) ) { + + $this->add_to_internal_cache( $derived_key, $value ); + + return true; + } + + $expiration = $this->sanitize_expiration( $expiration ); + + // Save to Memcached. + if ( $by_key ) { + $result = $this->m->addByKey( $server_key, $derived_key, $value, $expiration ); + } else { + $result = $this->m->add( $derived_key, $value, $expiration ); + } + + // Store in runtime cache if add was successful. + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $this->add_to_internal_cache( $derived_key, $value ); + } + + return $result; + } + + /** + * Adds a value to cache on a specific server. + * + * Using a server_key value, the object can be stored on a specified server as opposed + * to a random server in the stack. Note that this method will add the key/value to the + * _cache object as part of the runtime cache. It will add it to an array for the + * specified server_key. + * + * @link https://www.php.net/manual/en/memcached.addbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ + public function addByKey( $server_key, $key, $value, $group = 'default', $expiration = 0 ) { + return $this->add( $key, $value, $group, $expiration, $server_key, true ); + } + + /** + * Adds multiple values to cache. + * + * @param array $items Array of keys and values to be added. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param int $expiration Optional. When to expire the cache contents, in seconds. + * Default 0 (no expiration). + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false if cache key and group already exist. + */ + public function addMultiple( array $items, $group = '', $expiration = 0 ) { + $values = array(); + + foreach ( $items as $key => $value ) { + $values[ $key ] = $this->add( $key, $value, $group, $expiration ); + } + + return $values; + } + + /** + * Adds a single server to the list of Memcached servers. + * + * @link https://www.php.net/manual/en/memcached.addserver.php + * + * @param string $host The hostname of the memcache server. + * @param int $port The port on which memcache is running. + * @param int $weight The weight of the server relative to the total weight + * of all the servers in the pool. + * @return bool True on success, false on failure. + */ + public function addServer( $host, $port, $weight = 0 ) { + $host = is_string( $host ) ? $host : '127.0.0.1'; + $port = is_numeric( $port ) && $port > 0 ? $port : 11211; + $weight = is_numeric( $weight ) && $weight > 0 ? $weight : 1; + + return $this->m->addServer( $host, $port, $weight ); + } + + /** + * Adds an array of servers to the pool. + * + * Each individual server in the array must include a domain and port, with an optional + * weight value: $servers = array( array( '127.0.0.1', 11211, 0 ) ); + * + * @link https://www.php.net/manual/en/memcached.addservers.php + * + * @param array $servers Array of server to register. + * @return bool True on success, false on failure. + */ + public function addServers( $servers ) { + if ( ! is_object( $this->m ) ) { + return false; + } + + return $this->m->addServers( $servers ); + } + + /** + * Appends data to an existing item. + * + * This method should throw an error if it is used with compressed data. + * This is an expected behavior. Memcached casts the value to be appended to the initial value + * to the type of the initial value. Be careful as this leads to unexpected behavior at times. + * Due to how memcached treats types, the behavior has been mimicked in the internal cache to produce + * similar results and improve consistency. It is recommended that appends only occur with data of + * the same type. + * + * @link https://www.php.net/manual/en/memcached.append.php + * + * @param string $key The key under which to store the value. + * @param mixed $value Must be string as appending mixed values is not well-defined. + * @param string $group The group value appended to the $key. + * @param string $server_key The key identifying the server to store the value on. + * @param bool $by_key True to store in internal cache by key; false to not store by key. + * @return bool True on success, false on failure. + */ + public function append( $key, $value, $group = 'default', $server_key = '', $by_key = false ) { + if ( ! is_string( $value ) && ! is_int( $value ) && ! is_float( $value ) ) { + return false; + } + + $derived_key = $this->buildKey( $key, $group ); + + // If group is a non-Memcached group, append to runtime cache value, not Memcached. + if ( in_array( $group, $this->no_mc_groups, true ) ) { + if ( ! isset( $this->cache[ $derived_key ] ) ) { + return false; + } + + $combined = $this->combine_values( $this->cache[ $derived_key ], $value, 'app' ); + $this->add_to_internal_cache( $derived_key, $combined ); + return true; + } + + // Append to Memcached value. + if ( $by_key ) { + $result = $this->m->appendByKey( $server_key, $derived_key, $value ); + } else { + $result = $this->m->append( $derived_key, $value ); + } + + // Store in runtime cache if add was successful. + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $combined = $this->combine_values( $this->cache[ $derived_key ], $value, 'app' ); + $this->add_to_internal_cache( $derived_key, $combined ); + } + + return $result; + } + + /** + * Appends data to an existing item by server key. + * + * This method should throw an error if it is used with compressed data. + * This is an expected behavior. Memcached casts the value to be appended to the initial value + * to the type of the initial value. Be careful as this leads to unexpected behavior at times. + * Due to how memcached treats types, the behavior has been mimicked in the internal cache to produce + * similar results and improve consistency. It is recommended that appends only occur with data of + * the same type. + * + * @link https://www.php.net/manual/en/memcached.appendbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param mixed $value Must be string as appending mixed values is not well-defined. + * @param string $group The group value appended to the $key. + * @return bool True on success, false on failure. + */ + public function appendByKey( $server_key, $key, $value, $group = 'default' ) { + return $this->append( $key, $value, $group, $server_key, true ); + } + + /** + * Performs a "check and set" to store data. + * + * The set will be successful only if the no other request has updated the value + * since it was fetched by this request. + * + * @link https://www.php.net/manual/en/memcached.cas.php + * + * @param float $cas_token Unique value associated with the existing item. Generated by memcached. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @param string $server_key The key identifying the server to store the value on. + * @param bool $by_key True to store in internal cache by key; false to not store by key. + * @return bool True on success, false on failure. + */ + public function cas( $cas_token, $key, $value, $group = 'default', $expiration = 0, $server_key = '', $by_key = false ) { + $derived_key = $this->buildKey( $key, $group ); + + /** + * If group is a non-Memcached group, save to runtime cache, not Memcached. Note + * that since check and set cannot be emulated in the run time cache, this value + * operation is treated as a normal "add" for no_mc_groups. + */ + if ( in_array( $group, $this->no_mc_groups, true ) ) { + $this->add_to_internal_cache( $derived_key, $value ); + return true; + } + + $expiration = $this->sanitize_expiration( $expiration ); + + // Save to Memcached. + if ( $by_key ) { + $result = $this->m->casByKey( $cas_token, $server_key, $derived_key, $value, $expiration ); + } else { + $result = $this->m->cas( $cas_token, $derived_key, $value, $expiration ); + } + + // Store in runtime cache if cas was successful. + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $this->add_to_internal_cache( $derived_key, $value ); + } + + return $result; + } + + /** + * Performs a "check and set" to store data with a server key. + * + * The set will be successful only if the no other request has updated the value + * since it was fetched by this request. + * + * @link https://www.php.net/manual/en/memcached.casbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param float $cas_token Unique value associated with the existing item. Generated by memcached. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ + public function casByKey( $cas_token, $server_key, $key, $value, $group = 'default', $expiration = 0 ) { + return $this->cas( $cas_token, $key, $value, $group, $expiration, $server_key, true ); + } + + /** + * Decrements a numeric item's value. + * + * @link https://www.php.net/manual/en/memcached.decrement.php + * + * @param string $key The key under which to store the value. + * @param int $offset The amount by which to decrement the item's value. + * @param string $group The group value appended to the $key. + * @return int|bool Item's new value on success, false on failure. + */ + public function decrement( $key, $offset = 1, $group = 'default' ) { + $derived_key = $this->buildKey( $key, $group ); + + // Decrement values in no_mc_groups. + if ( in_array( $group, $this->no_mc_groups, true ) ) { + + // Only decrement if the key already exists and value is 0 or greater (mimics memcached behavior). + if ( isset( $this->cache[ $derived_key ] ) && $this->cache[ $derived_key ] >= 0 ) { + + // If numeric, subtract; otherwise, consider it 0 and do nothing. + if ( is_numeric( $this->cache[ $derived_key ] ) ) { + $this->cache[ $derived_key ] -= (int) $offset; + } else { + $this->cache[ $derived_key ] = 0; + } + + // Returned value cannot be less than 0. + if ( $this->cache[ $derived_key ] < 0 ) { + $this->cache[ $derived_key ] = 0; + } + + return $this->cache[ $derived_key ]; + } else { + return false; + } + } + + $result = $this->m->decrement( $derived_key, $offset ); + + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $this->add_to_internal_cache( $derived_key, $result ); + } + + return $result; + } + + /** + * Decrements a numeric item's value. + * + * Alias for $this->decrement(). Other caching backends use this abbreviated form + * of the function. It *may* cause breakage somewhere, so it is nice to have. + * This function will also allow the core unit tests to pass. + * + * @param string $key The key under which to store the value. + * @param int $offset The amount by which to decrement the item's value. + * @param string $group The group value appended to the $key. + * @return int|bool Item's new value on success, false on failure. + */ + public function decr( $key, $offset = 1, $group = 'default' ) { + return $this->decrement( $key, $offset, $group ); + } + + /** + * Removes the item from the cache. + * + * Removes an item from memcached with identified by $key after $time seconds. + * The $time parameter allows an object to be queued for deletion without + * immediately deleting. Between the time that it is queued and the time it's deleted, + * add, replace, and get will fail, but set will succeed. + * + * @link https://www.php.net/manual/en/memcached.delete.php + * + * @param string $key The key under which to store the value. + * @param string $group The group value appended to the $key. + * @param int $time The amount of time the server will wait to delete the item in seconds. + * @param string $server_key The key identifying the server to store the value on. + * @param bool $by_key True to store in internal cache by key; false to not store by key. + * @return bool True on success, false on failure. + */ + public function delete( $key, $group = 'default', $time = 0, $server_key = '', $by_key = false ) { + $derived_key = $this->buildKey( $key, $group ); + + // Remove from no_mc_groups array. + if ( in_array( $group, $this->no_mc_groups, true ) ) { + if ( isset( $this->cache[ $derived_key ] ) ) { + unset( $this->cache[ $derived_key ] ); + } + + return true; + } + + if ( $by_key ) { + $result = $this->m->deleteByKey( $server_key, $derived_key, $time ); + } else { + $result = $this->m->delete( $derived_key, $time ); + } + + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + unset( $this->cache[ $derived_key ] ); + } + + return $result; + } + + /** + * Removes the item from the cache by server key. + * + * Removes an item from memcached with identified by $key after $time seconds. + * The $time parameter allows an object to be queued for deletion without + * immediately deleting. Between the time that it is queued and the time it's deleted, + * add, replace, and get will fail, but set will succeed. + * + * @link https://www.php.net/manual/en/memcached.deletebykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param string $group The group value appended to the $key. + * @param int $time The amount of time the server will wait to delete the item in seconds. + * @return bool True on success, false on failure. + */ + public function deleteByKey( $server_key, $key, $group = 'default', $time = 0 ) { + return $this->delete( $key, $group, $time, $server_key, true ); + } + + /** + * Removes multiple items from the cache. + * + * @param array $keys Array of keys under which the cache to deleted. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false if the contents were not deleted. + */ + public function deleteMultiple( $keys, $group ) { + $values = array(); + + foreach ( $keys as $key ) { + $values[ $key ] = $this->delete( $key, $group ); + } + + return $values; + } + + /** + * Fetches the next result. + * + * @link https://www.php.net/manual/en/memcached.fetch.php + * + * @return array|false The next result on success, false on failure. + */ + public function fetch() { + return $this->m->fetch(); + } + + /** + * Fetches all remaining results from the last request. + * + * @link https://www.php.net/manual/en/memcached.fetchall.php + * + * @return array|false The results on success, false on failure. + */ + public function fetchAll() { + return $this->m->fetchAll(); + } + + /** + * Invalidates all items in the cache. + * + * @link https://www.php.net/manual/en/memcached.flush.php + * + * @param int $delay Number of seconds to wait before invalidating the items. + * @return bool True on success, false on failure. + */ + public function flush( $delay = 0 ) { + $result = $this->m->flush( $delay ); + + // Only reset the runtime cache if memcached was properly flushed. + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $this->cache = array(); + } + + return $result; + } + + /** + * Clears the in-memory cache of all data leaving the external cache untouched. + * + * @return bool Always returns true. + */ + public function flush_runtime() { + $this->cache = array(); + + return true; + } + + /** + * Retrieves object from cache. + * + * Gets an object from cache based on $key and $group. In order to fully support + * the $cache_cb and $cas_token parameters, the runtime cache is ignored by this function + * if either of those values are set. In that case, the request is made directly + * to the memcached server for proper handling of the callback and/or token. + * Note that the $cas_token variable cannot be directly passed to the function. + * The variable needs to be first defined with a non-null value. + * + * If using the $cache_cb argument, the new value will always have an expiration + * of time of 0 (forever). This is a limitation of the Memcached PECL extension. + * + * @link https://www.php.net/manual/en/memcached.get.php + * + * @param string $key The key under which to store the value. + * @param string $group The group value appended to the $key. + * @param bool $force Whether or not to force a cache invalidation. + * @param null|bool $found Variable passed by reference to determine if the value was found or not. + * @param string $server_key The key identifying the server to store the value on. + * @param bool $by_key True to store in internal cache by key; false to not store by key. + * @param null|callable $cache_cb Read-through caching callback. + * @param null|float $cas_token The variable to store the CAS token in. + * @return bool|mixed Cached object value. + */ + public function get( $key, $group = 'default', $force = false, &$found = null, $server_key = '', $by_key = false, $cache_cb = null, &$cas_token = null ) { + $derived_key = $this->buildKey( $key, $group ); + + // Assume object is not found. + $found = false; + + // If either $cache_db, or $cas_token is set, must hit Memcached and bypass runtime cache. + if ( func_num_args() > 6 && ! in_array( $group, $this->no_mc_groups, true ) ) { + if ( $by_key ) { + $value = $this->m->getByKey( $server_key, $derived_key, $cache_cb, $cas_token ); + } else { + $value = $this->m->get( $derived_key, $cache_cb, $cas_token ); + } + } else { + if ( isset( $this->cache[ $derived_key ] ) ) { + $found = true; + return is_object( $this->cache[ $derived_key ] ) ? clone $this->cache[ $derived_key ] : $this->cache[ $derived_key ]; + } elseif ( in_array( $group, $this->no_mc_groups, true ) ) { + return false; + } else { + if ( $by_key ) { + $value = $this->m->getByKey( $server_key, $derived_key ); + } else { + $value = $this->m->get( $derived_key ); + } + } + } + + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $this->add_to_internal_cache( $derived_key, $value ); + $found = true; + } + + return is_object( $value ) ? clone $value : $value; + } + + /** + * Retrieves object from cache from specified server. + * + * Gets an object from cache based on $key and $group, and $server_key. In order to fully support + * the $cache_cb and $cas_token parameters, the runtime cache is ignored by this function + * if either of those values are set. In that case, the request is made directly + * to the memcached server for proper handling of the callback and/or token. + * Note that the $cas_token variable cannot be directly passed to the function. + * The variable needs to be first defined with a non-null value. + * + * If using the $cache_cb argument, the new value will always have an expiration + * of time of 0 (forever). This is a limitation of the Memcached PECL extension. + * + * @link https://www.php.net/manual/en/memcached.getbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param string $group The group value appended to the $key. + * @param bool $force Whether or not to force a cache invalidation. + * @param null|bool $found Variable passed by reference to determine if the value was found or not. + * @param null|string $cache_cb Read-through caching callback. + * @param null|float $cas_token The variable to store the CAS token in. + * @return bool|mixed Cached object value. + */ + public function getByKey( $server_key, $key, $group = 'default', $force = false, &$found = null, $cache_cb = null, &$cas_token = null ) { + /** + * Need to be careful how "get" is called. If you send $cache_cb, and $cas_token, it will hit memcached. + * Only send those args if they were sent to this function. + */ + if ( func_num_args() > 5 ) { + return $this->get( $key, $group, $force, $found, $server_key, true, $cache_cb, $cas_token ); + } else { + return $this->get( $key, $group, $force, $found, $server_key, true ); + } + } + + /** + * Requests multiple keys without blocking. + * + * @link https://www.php.net/manual/en/memcached.getdelayed.php + * + * @param string|array $keys Array or string of key(s) to request. + * @param string|array $groups Array or string of group(s) for the key(s). + * See buildKeys for more on how these are handled. + * @param bool $with_cas Whether to request CAS token values also. + * @param null $value_cb The result callback or null. + * @return bool True on success, false on failure. + */ + public function getDelayed( $keys, $groups = 'default', $with_cas = false, $value_cb = null ) { + $derived_keys = $this->buildKeys( $keys, $groups ); + return $this->m->getDelayed( $derived_keys, $with_cas, $value_cb ); + } + + /** + * Requests multiple keys without blocking from a specified server. + * + * @link https://www.php.net/manual/en/memcached.getdelayed.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string|array $keys Array or string of key(s) to request. + * @param string|array $groups Array or string of group(s) for the key(s). + * See buildKeys for more on how these are handled. + * @param bool $with_cas Whether to request CAS token values also. + * @param null $value_cb The result callback or null. + * @return bool True on success, false on failure. + */ + public function getDelayedByKey( $server_key, $keys, $groups = 'default', $with_cas = false, $value_cb = null ) { + $derived_keys = $this->buildKeys( $keys, $groups ); + return $this->m->getDelayedByKey( $server_key, $derived_keys, $with_cas, $value_cb ); + } + + /** + * Gets multiple values from memcached in one request. + * + * See the buildKeys method definition to understand the $keys/$groups parameters. + * + * @link https://www.php.net/manual/en/memcached.getmulti.php + * + * @param array $keys Array of keys to retrieve. + * @param string|array $groups If string, used for all keys. + * If arrays, corresponds with the $keys array. + * @param string $server_key The key identifying the server to store the value on. + * @param null|array $cas_tokens The variable to store the CAS tokens for the found items. + * @param int $flags The flags for the get operation. + * @return bool|array The array of found items on success, false on failure. + */ + public function getMulti( $keys, $groups = 'default', $server_key = '', &$cas_tokens = null, $flags = null ) { + $derived_keys = $this->buildKeys( $keys, $groups ); + + /** + * If either $cas_tokens, or $flags is set, must hit Memcached and bypass runtime cache. + * Note that this will purposely ignore no_mc_groups values as they cannot handle CAS tokens + * or the special flags; however, if the groups of groups contains a no_mc_group, this is bypassed. + */ + if ( func_num_args() > 3 && ! $this->contains_no_mc_group( $groups ) ) { + if ( ! empty( $server_key ) ) { + $values = $this->m->getMultiByKey( $server_key, $derived_keys, $cas_tokens, $flags ); + } else { + $values = $this->m->getMulti( $derived_keys, $cas_tokens, $flags ); + } + } else { + $values = array(); + $need_to_get = array(); + + // Pull out values from runtime cache, or mark for retrieval. + foreach ( $derived_keys as $key ) { + if ( isset( $this->cache[ $key ] ) ) { + $values[ $key ] = $this->cache[ $key ]; + } else { + $need_to_get[ $key ] = $key; + } + } + + // Get those keys not found in the runtime cache. + if ( ! empty( $need_to_get ) ) { + if ( ! empty( $server_key ) ) { + $result = $this->m->getMultiByKey( $server_key, array_keys( $need_to_get ) ); + } else { + $result = $this->m->getMulti( array_keys( $need_to_get ) ); + } + } + + // Merge with values found in runtime cache. + if ( isset( $result ) && Memcached::RES_SUCCESS === $this->getResultCode() ) { + $values = array_merge( $values, $result ); + } + + // If order should be preserved, reorder now. + if ( ! empty( $need_to_get ) && Memcached::GET_PRESERVE_ORDER === $flags ) { + $ordered_values = array(); + + foreach ( $derived_keys as $key ) { + if ( isset( $values[ $key ] ) ) { + $ordered_values[ $key ] = $values[ $key ]; + } + } + + $values = $ordered_values; + unset( $ordered_values ); + } + } + + // Add the values to the runtime cache. + $this->cache = array_merge( $this->cache, $values ); + + return $values; + } + + /** + * Gets multiple values from memcached in one request by specified server key. + * + * See the buildKeys method definition to understand the $keys/$groups parameters. + * + * @link https://www.php.net/manual/en/memcached.getmultibykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param array $keys Array of keys to retrieve. + * @param string|array $groups If string, used for all keys. + * If arrays, corresponds with the $keys array. + * @param null|array $cas_tokens The variable to store the CAS tokens for the found items. + * @param int $flags The flags for the get operation. + * @return bool|array The array of found items on success, false on failure. + */ + public function getMultiByKey( $server_key, $keys, $groups = 'default', &$cas_tokens = null, $flags = null ) { + /** + * Need to be careful how "getMulti" is called. If you send $cache_cb, and $cas_token, it will hit memcached. + * Only send those args if they were sent to this function. + */ + if ( func_num_args() > 3 ) { + return $this->getMulti( $keys, $groups, $server_key, $cas_tokens, $flags ); + } else { + return $this->getMulti( $keys, $groups, $server_key ); + } + } + + /** + * Get multiple items from the cache. + * + * @param array $keys Array of keys under which the cache contents are stored. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param bool $force Optional. Whether to force an update of the local cache + * from the persistent cache. Default false. + * @return array Array of return values, grouped by key. Each value is either + * the cache contents on success, or false on failure. + */ + public function getMultiple( $keys, $group = '', $force = false ) { + $values = array(); + + foreach ( $keys as $key ) { + $found = null; + $value = $this->get( $key, $group, $force, $found ); + $values[ $key ] = $found ? $value : false; + } + + return $values; + } + + /** + * Retrieves a Memcached option value. + * + * @link https://www.php.net/manual/en/memcached.getoption.php + * + * @param int $option One of the Memcached::OPT_* constants. + * @return mixed The value of the requested option on success, false on failure. + */ + public function getOption( $option ) { + return $this->m->getOption( $option ); + } + + /** + * Returns the result code of the last option. + * + * @link https://www.php.net/manual/en/memcached.getresultcode.php + * + * @return int Result code of the last Memcached operation. + */ + public function getResultCode() { + return $this->m->getResultCode(); + } + + /** + * Return the message describing the result of the last operation. + * + * @link https://www.php.net/manual/en/memcached.getresultmessage.php + * + * @return string Message describing the result of the last Memcached operation. + */ + public function getResultMessage() { + return $this->m->getResultMessage(); + } + + /** + * Gets server information by key. + * + * @link https://www.php.net/manual/en/memcached.getserverbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @return array Array with host, post, and weight on success, false on failure. + */ + public function getServerByKey( $server_key ) { + return $this->m->getServerByKey( $server_key ); + } + + /** + * Gets the list of servers in the pool. + * + * @link https://www.php.net/manual/en/memcached.getserverlist.php + * + * @return array The list of all servers in the server pool. + */ + public function getServerList() { + return $this->m->getServerList(); + } + + /** + * Gets server pool statistics. + * + * @link https://www.php.net/manual/en/memcached.getstats.php + * + * @return array Array of server statistics, one entry per server. + */ + public function getStats() { + return $this->m->getStats(); + } + + /** + * Gets server pool memcached version information. + * + * @link https://www.php.net/manual/en/memcached.getversion.php + * + * @return array Array of server versions, one entry per server. + */ + public function getVersion() { + return $this->m->getVersion(); + } + + /** + * Increments a numeric item's value. + * + * @link https://www.php.net/manual/en/memcached.increment.php + * + * @param string $key The key under which to store the value. + * @param int $offset The amount by which to increment the item's value. + * @param string $group The group value appended to the $key. + * @return int|bool Item's new value on success, false on failure. + */ + public function increment( $key, $offset = 1, $group = 'default' ) { + $derived_key = $this->buildKey( $key, $group ); + + // Increment values in no_mc_groups. + if ( in_array( $group, $this->no_mc_groups, true ) ) { + + // Only increment if the key already exists and the number is currently 0 or greater (mimics memcached behavior). + if ( isset( $this->cache[ $derived_key ] ) && $this->cache[ $derived_key ] >= 0 ) { + + // If numeric, add; otherwise, consider it 0 and do nothing. + if ( is_numeric( $this->cache[ $derived_key ] ) ) { + $this->cache[ $derived_key ] += (int) $offset; + } else { + $this->cache[ $derived_key ] = 0; + } + + // Returned value cannot be less than 0. + if ( $this->cache[ $derived_key ] < 0 ) { + $this->cache[ $derived_key ] = 0; + } + + return $this->cache[ $derived_key ]; + } else { + return false; + } + } + + $result = $this->m->increment( $derived_key, $offset ); + + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $this->add_to_internal_cache( $derived_key, $result ); + } + + return $result; + } + + /** + * Alias for $this->incr(). + * + * Certain plugins expect an "incr" method on the $wp_object_cache object (e.g., Batcache). + * Since the original version of this library matched names to the memcached methods, + * the "incr" method was missing. Adding this method restores compatibility with plugins + * expecting an "incr" method. + * + * @param string $key The key under which to store the value. + * @param int $offset The amount by which to increment the item's value. + * @param string $group The group value appended to the $key. + * @return int|bool Item's new value on success, false on failure. + */ + public function incr( $key, $offset = 1, $group = 'default' ) { + return $this->increment( $key, $offset, $group ); + } + + /** + * Prepends data to an existing item. + * + * This method should throw an error if it is used with compressed data. This is an expected behavior. + * Memcached casts the value to be prepended to the initial value to the type of the initial value. + * Be careful as this leads to unexpected behavior at times. For instance, prepending (float) 45.23 + * to (int) 23 will result in 45, because the value is first combined (45.2323) then cast to "integer" + * (the original value), which will be (int) 45. Due to how memcached treats types, the behavior has been + * mimicked in the internal cache to produce similar results and improve consistency. It is recommended + * that prepends only occur with data of the same type. + * + * @link https://www.php.net/manual/en/memcached.prepend.php + * + * @param string $key The key under which to store the value. + * @param string $value Must be string as prepending mixed values is not well-defined. + * @param string $group The group value prepended to the $key. + * @param string $server_key The key identifying the server to store the value on. + * @param bool $by_key True to store in internal cache by key; false to not store by key. + * @return bool True on success, false on failure. + */ + public function prepend( $key, $value, $group = 'default', $server_key = '', $by_key = false ) { + if ( ! is_string( $value ) && ! is_int( $value ) && ! is_float( $value ) ) { + return false; + } + + $derived_key = $this->buildKey( $key, $group ); + + // If group is a non-Memcached group, prepend to runtime cache value, not Memcached. + if ( in_array( $group, $this->no_mc_groups, true ) ) { + if ( ! isset( $this->cache[ $derived_key ] ) ) { + return false; + } + + $combined = $this->combine_values( $this->cache[ $derived_key ], $value, 'pre' ); + $this->add_to_internal_cache( $derived_key, $combined ); + return true; + } + + // Append to Memcached value. + if ( $by_key ) { + $result = $this->m->prependByKey( $server_key, $derived_key, $value ); + } else { + $result = $this->m->prepend( $derived_key, $value ); + } + + // Store in runtime cache if add was successful. + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $combined = $this->combine_values( $this->cache[ $derived_key ], $value, 'pre' ); + $this->add_to_internal_cache( $derived_key, $combined ); + } + + return $result; + } + + /** + * Appends data to an existing item by server key. + * + * This method should throw an error if it is used with compressed data. This is an expected behavior. + * Memcached casts the value to be prepended to the initial value to the type of the initial value. + * Be careful as this leads to unexpected behavior at times. For instance, prepending (float) 45.23 + * to (int) 23 will result in 45, because the value is first combined (45.2323) then cast to "integer" + * (the original value), which will be (int) 45. Due to how memcached treats types, the behavior has been + * mimicked in the internal cache to produce similar results and improve consistency. It is recommended + * that prepends only occur with data of the same type. + * + * @link https://www.php.net/manual/en/memcached.prependbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param string $value Must be string as prepending mixed values is not well-defined. + * @param string $group The group value prepended to the $key. + * @return bool True on success, false on failure. + */ + public function prependByKey( $server_key, $key, $value, $group = 'default' ) { + return $this->prepend( $key, $value, $group, $server_key, true ); + } + + /** + * Replaces a value in cache. + * + * This method is similar to "add"; however, is does not successfully set a value + * if the object's key is not already set in cache. + * + * @link https://www.php.net/manual/en/memcached.replace.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param bool $by_key True to store in internal cache by key; false to not store by key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ + public function replace( $key, $value, $group = 'default', $expiration = 0, $server_key = '', $by_key = false ) { + $derived_key = $this->buildKey( $key, $group ); + + // If group is a non-Memcached group, save to runtime cache, not Memcached. + if ( in_array( $group, $this->no_mc_groups, true ) ) { + + // Replace won't save unless the key already exists; mimic this behavior here. + if ( ! isset( $this->cache[ $derived_key ] ) ) { + return false; + } + + $this->cache[ $derived_key ] = $value; + return true; + } + + $expiration = $this->sanitize_expiration( $expiration ); + + // Save to Memcached. + if ( $by_key ) { + $result = $this->m->replaceByKey( $server_key, $derived_key, $value, $expiration ); + } else { + $result = $this->m->replace( $derived_key, $value, $expiration ); + } + + // Store in runtime cache if add was successful. + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $this->add_to_internal_cache( $derived_key, $value ); + } + + return $result; + } + + /** + * Replaces a value in cache on a specific server. + * + * This method is similar to "addByKey"; however, is does not successfully set a value + * if the object's key is not already set in cache. + * + * @link https://www.php.net/manual/en/memcached.addbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ + public function replaceByKey( $server_key, $key, $value, $group = 'default', $expiration = 0 ) { + return $this->replace( $key, $value, $group, $expiration, $server_key, true ); + } + + /** + * Sets a value in cache. + * + * The value is set whether or not this key already exists in memcached. + * + * @link https://www.php.net/manual/en/memcached.set.php + * + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @param string $server_key The key identifying the server to store the value on. + * @param bool $by_key True to store in internal cache by key; false to not store by key. + * @return bool True on success, false on failure. + */ + public function set( $key, $value, $group = 'default', $expiration = 0, $server_key = '', $by_key = false ) { + $derived_key = $this->buildKey( $key, $group ); + + // If group is a non-Memcached group, save to runtime cache, not Memcached. + if ( in_array( $group, $this->no_mc_groups, true ) ) { + $this->add_to_internal_cache( $derived_key, $value ); + return true; + } + + $expiration = $this->sanitize_expiration( $expiration ); + + // Save to Memcached. + if ( $by_key ) { + $result = $this->m->setByKey( $server_key, $derived_key, $value, $expiration ); + } else { + $result = $this->m->set( $derived_key, $value, $expiration ); + } + + // Store in runtime cache if add was successful. + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $this->add_to_internal_cache( $derived_key, $value ); + } + + return $result; + } + + /** + * Sets a value in cache on a specific server. + * + * The value is set whether or not this key already exists in memcached. + * + * @link https://www.php.net/manual/en/memcached.setbykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param string $key The key under which to store the value. + * @param mixed $value The value to store. + * @param string $group The group value appended to the $key. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ + public function setByKey( $server_key, $key, $value, $group = 'default', $expiration = 0 ) { + return $this->set( $key, $value, $group, $expiration, $server_key, true ); + } + + /** + * Sets multiple values to cache at once. + * + * By sending an array of $items to this function, all values are saved at once to + * memcached, reducing the need for multiple requests to memcached. The $items array + * keys and values are what are stored to memcached. The keys in the $items array + * are merged with the $groups array/string value via buildKeys to determine the + * final key for the object. + * + * @link https://www.php.net/manual/en/memcached.setmulti.php + * + * @param array $items An array of key/value pairs to store on the server. + * @param string|array $groups Group(s) to merge with key(s) in $items. + * @param int $expiration The expiration time, defaults to 0. + * @param string $server_key The key identifying the server to store the value on. + * @param bool $by_key True to store in internal cache by key; false to not store by key. + * @return bool True on success, false on failure. + */ + public function setMulti( $items, $groups = 'default', $expiration = 0, $server_key = '', $by_key = false ) { + // Build final keys and replace $items keys with the new keys. + $derived_keys = $this->buildKeys( array_keys( $items ), $groups ); + $derived_items = array_combine( $derived_keys, $items ); + + // Do not add to memcached if in no_mc_groups. + foreach ( $derived_items as $derived_key => $value ) { + + // Get the individual item's group. + $key_pieces = explode( ':', $derived_key ); + + // If group is a non-Memcached group, save to runtime cache, not Memcached. + if ( in_array( $key_pieces[1], $this->no_mc_groups, true ) ) { + $this->add_to_internal_cache( $derived_key, $value ); + unset( $derived_items[ $derived_key ] ); + } + } + + $expiration = $this->sanitize_expiration( $expiration ); + + // Save to memcached. + if ( $by_key ) { + $result = $this->m->setMultiByKey( $server_key, $derived_items, $expiration ); + } else { + $result = $this->m->setMulti( $derived_items, $expiration ); + } + + // Store in runtime cache if add was successful. + if ( Memcached::RES_SUCCESS === $this->getResultCode() ) { + $this->cache = array_merge( $this->cache, $derived_items ); + } + + return $result; + } + + /** + * Sets multiple values to cache at once on specified server. + * + * By sending an array of $items to this function, all values are saved at once to + * memcached, reducing the need for multiple requests to memcached. The $items array + * keys and values are what are stored to memcached. The keys in the $items array + * are merged with the $groups array/string value via buildKeys to determine the + * final key for the object. + * + * @link https://www.php.net/manual/en/memcached.setmultibykey.php + * + * @param string $server_key The key identifying the server to store the value on. + * @param array $items An array of key/value pairs to store on the server. + * @param string|array $groups Group(s) to merge with key(s) in $items. + * @param int $expiration The expiration time, defaults to 0. + * @return bool True on success, false on failure. + */ + public function setMultiByKey( $server_key, $items, $groups = 'default', $expiration = 0 ) { + return $this->setMulti( $items, $groups, $expiration, $server_key, true ); + } + + /** + * Sets multiple values in cache. + * + * @param array $items Array of keys and values to be set. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param int $expiration Optional. When to expire the cache contents, in seconds. + * Default 0 (no expiration). + * @return bool[] Array of return values, grouped by key. Each value is either + * true on success, or false on failure. + */ + public function setMultiple( array $items, $group = '', $expiration = 0 ) { + $values = array(); + + foreach ( $items as $key => $value ) { + $values[ $key ] = $this->set( $key, $value, $group, $expiration ); + } + + return $values; + } + + /** + * Sets a Memcached option. + * + * @link https://www.php.net/manual/en/memcached.setoption.php + * + * @param int $option Option name. + * @param mixed $value Option value. + * @return bool True on success, false on failure. + */ + public function setOption( $option, $value ) { + return $this->m->setOption( $option, $value ); + } + + /** + * Builds a key for the cached object using the blog_id, key, and group values. + * + * This function is inspired by the original WP Memcached Object cache. + * + * @author Ryan Boren + * @link http://wordpress.org/extend/plugins/memcached/ + * + * @param string $key The key under which to store the value. + * @param string $group The group value appended to the $key. + * @return string + */ + public function buildKey( $key, $group = 'default' ) { + if ( empty( $group ) ) { + $group = 'default'; + } + + if ( false !== array_search( $group, $this->global_groups, true ) ) { + $prefix = $this->global_prefix; + } else { + $prefix = $this->blog_prefix; + } + + return preg_replace( '/\s+/', '', WP_CACHE_KEY_SALT . "$prefix$group:$key" ); + } + + /** + * Creates an array of keys from passed key(s) and group(s). + * + * This function takes a string or array of key(s) and group(s) and combines them into a single dimensional + * array that merges the keys and groups. If the same number of keys and groups exist, the final keys will + * append $groups[n] to $keys[n]. If there are more keys than groups and the $groups parameter is an array, + * $keys[n] will be combined with $groups[n] until $groups runs out of values. 'default' will be used for remaining + * values. If $keys is an array and $groups is a string, all final values will append $groups to $keys[n]. + * If both values are strings, they will be combined into a single string. Note that if more $groups are received + * than $keys, the method will return an empty array. This method is primarily a helper method for methods + * that call memcached with an array of keys. + * + * @param string|array $keys Key(s) to merge with group(s). + * @param string|array $groups Group(s) to merge with key(s). + * @return array Array that combines keys and groups into a single set of memcached keys. + */ + public function buildKeys( $keys, $groups = 'default' ) { + $derived_keys = array(); + + // If strings sent, convert to arrays for proper handling. + if ( ! is_array( $groups ) ) { + $groups = (array) $groups; + } + + if ( ! is_array( $keys ) ) { + $keys = (array) $keys; + } + $keys = array_values( $keys ); + + // If we have equal numbers of keys and groups, merge $keys[n] and $group[n]. + if ( count( $keys ) === count( $groups ) ) { + for ( $i = 0; $i < count( $keys ); $i++ ) { + $derived_keys[] = $this->buildKey( $keys[ $i ], $groups[ $i ] ); + } + + // If more keys are received than groups, merge $keys[n] and $group[n] + // until no more groups are left; remaining groups are 'default'. + } elseif ( count( $keys ) > count( $groups ) ) { + for ( $i = 0; $i < count( $keys ); $i++ ) { + if ( isset( $groups[ $i ] ) ) { + $derived_keys[] = $this->buildKey( $keys[ $i ], $groups[ $i ] ); + } elseif ( count( $groups ) === 1 ) { + $derived_keys[] = $this->buildKey( $keys[ $i ], $groups[0] ); + } else { + $derived_keys[] = $this->buildKey( $keys[ $i ], 'default' ); + } + } + } + + return $derived_keys; + } + + /** + * Ensures that a proper expiration time is set. + * + * Memcached treats any value over 30 days as a timestamp. If a developer sets the expiration + * for greater than 30 days or less than the current timestamp, the timestamp is in the past + * and the value isn't cached. This function detects values in that range and corrects them. + * + * @param string|int $expiration The dirty expiration time. + * @return string|int The sanitized expiration time. + */ + public function sanitize_expiration( $expiration ) { + if ( $expiration > $this->thirty_days && $expiration <= $this->now ) { + $expiration = $expiration + $this->now; + } + + return $expiration; + } + + /** + * Concatenates two values and casts to type of the first value. + * + * This is used in append and prepend operations to match how these functions are handled + * by memcached. In both cases, whichever value is the original value in the combined value + * will dictate the type of the combined value. + * + * @param mixed $original Original value that dictates the combined type. + * @param mixed $pended Value to combine with original value. + * @param string $direction Either 'pre' or 'app'. + * @return mixed Combined value casted to the type of the first value. + */ + public function combine_values( $original, $pended, $direction ) { + $type = gettype( $original ); + + // Combine the values based on direction of the "pend". + if ( 'pre' === $direction ) { + $combined = $pended . $original; + } else { + $combined = $original . $pended; + } + + // Cast type of combined value. + settype( $combined, $type ); + + return $combined; + } + + /** + * Simple wrapper for saving object to the internal cache. + * + * @param string $derived_key Key to save value under. + * @param mixed $value Object value. + */ + public function add_to_internal_cache( $derived_key, $value ) { + if ( is_object( $value ) ) { + $value = clone $value; + } + + $this->cache[ $derived_key ] = $value; + } + + /** + * Determines if a no_mc_group exists in a group of groups. + * + * @param mixed $groups The groups to search. + * @return bool True if a no_mc_group is present; false if a no_mc_group is not present. + */ + public function contains_no_mc_group( $groups ) { + if ( is_scalar( $groups ) ) { + return in_array( $groups, $this->no_mc_groups, true ); + } + + if ( ! is_array( $groups ) ) { + return false; + } + + foreach ( $groups as $group ) { + if ( in_array( $group, $this->no_mc_groups, true ) ) { + return true; + } + } + + return false; + } + + /** + * Adds global groups. + * + * This function comes straight from the original WP Memcached Object cache. + * + * @author Ryan Boren + * @link http://wordpress.org/extend/plugins/memcached/ + * + * @param array $groups Array of groups. + */ + public function add_global_groups( $groups ) { + if ( ! is_array( $groups ) ) { + $groups = (array) $groups; + } + + $this->global_groups = array_merge( $this->global_groups, $groups ); + $this->global_groups = array_unique( $this->global_groups ); + } + + /** + * Adds non-persistent groups. + * + * This function comes straight from the original WP Memcached Object cache. + * + * @author Ryan Boren + * @link http://wordpress.org/extend/plugins/memcached/ + * + * @param array $groups Array of groups. + */ + public function add_non_persistent_groups( $groups ) { + if ( ! is_array( $groups ) ) { + $groups = (array) $groups; + } + + $this->no_mc_groups = array_merge( $this->no_mc_groups, $groups ); + $this->no_mc_groups = array_unique( $this->no_mc_groups ); + } + + /** + * Gets a value specifically from the internal, run-time cache, not memcached. + * + * @param int|string $key Key value. + * @param int|string $group Group that the value belongs to. + * @return bool|mixed Value on success, false on failure. + */ + public function get_from_runtime_cache( $key, $group ) { + $derived_key = $this->buildKey( $key, $group ); + + if ( isset( $this->cache[ $derived_key ] ) ) { + return $this->cache[ $derived_key ]; + } + + return false; + } + + /** + * Switches blog prefix, which changes the cache that is accessed. + * + * @param int $blog_id Blog to switch to. + */ + public function switch_to_blog( $blog_id ) { + global $table_prefix; + $blog_id = (int) $blog_id; + $this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix ) . ':'; + } +} +// phpcs:enable From f227c5b2307143f3726a98a5f8ebb16296320e15 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 6 Nov 2023 22:35:43 +0100 Subject: [PATCH 07/21] Fix paths --- packages/env/lib/commands/start.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index b08cbab9bda4b..50b4020d41cf4 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -175,13 +175,18 @@ module.exports = async function start( { memcachedFile, path.join( config.env.development.coreSource.path, - 'wp-content' + 'wp-content', + 'object-cache.php' ) ); fs.copyFile( memcachedFile, - path.join( config.env.tests.coreSource.path, 'wp-content' ) + path.join( + config.env.tests.coreSource.path, + 'wp-content', + 'object-cache.php' + ) ); } } From c3f69e4babc2c40a131da50d524526561dd14ac2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 6 Nov 2023 22:41:35 +0100 Subject: [PATCH 08/21] Fix path --- packages/env/lib/commands/start.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index 50b4020d41cf4..fe0eba66b39fd 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -169,7 +169,10 @@ module.exports = async function start( { if ( 'memcached' === config.env.development.objectCache ) { // Set up object cache drop-in if configured. - const memcachedFile = path.resolve( __dirname, '../memcached.php' ); + const memcachedFile = path.resolve( + __dirname, + '../object-cache/memcached.php' + ); fs.copyFile( memcachedFile, From 61ed52de2f858eddeb27ee057ea44a846821c913 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 09:31:00 +0100 Subject: [PATCH 09/21] Mock child_process in tests Allows to run them even when Docker is not installed --- packages/env/lib/test/cli.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/env/lib/test/cli.js b/packages/env/lib/test/cli.js index ba850e3259f4c..4fefa6f0f72ce 100644 --- a/packages/env/lib/test/cli.js +++ b/packages/env/lib/test/cli.js @@ -9,6 +9,12 @@ const env = require( '../env' ); * Mocked dependencies */ jest.spyOn( process, 'exit' ).mockImplementation( () => {} ); +jest.mock( 'child_process', () => { + return { + exec: jest.fn( Promise.resolve.bind( Promise ) ), + execSync: jest.fn( () => 'Success!' ), + }; +} ); jest.mock( 'ora', () => () => ( { start() { return { text: '', succeed: jest.fn(), fail: jest.fn() }; From 9edeb5ca3a584cccab7f7f0660f08cd63a83f13d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 09:31:17 +0100 Subject: [PATCH 10/21] Update tests --- .../env/lib/build-docker-compose-config.js | 74 +++++++++++-------- .../__snapshots__/config-integration.js.snap | 8 ++ packages/env/lib/config/test/parse-config.js | 28 +++++++ packages/env/lib/config/validate-config.js | 8 +- .../lib/test/build-docker-compose-config.js | 22 ++++++ 5 files changed, 105 insertions(+), 35 deletions(-) diff --git a/packages/env/lib/build-docker-compose-config.js b/packages/env/lib/build-docker-compose-config.js index 46c7684bcd6a0..27a8b802b06be 100644 --- a/packages/env/lib/build-docker-compose-config.js +++ b/packages/env/lib/build-docker-compose-config.js @@ -194,7 +194,10 @@ module.exports = function buildDockerComposeConfig( config ) { volumes: [ 'mysql-test:/var/lib/mysql' ], }, wordpress: { - depends_on: [ 'mysql' ], + depends_on: [ + 'mysql', + config.env.development.objectCache && 'object-cache', + ].filter( Boolean ), build: { context: '.', dockerfile: 'WordPress.Dockerfile', @@ -212,7 +215,10 @@ module.exports = function buildDockerComposeConfig( config ) { extra_hosts: [ 'host.docker.internal:host-gateway' ], }, 'tests-wordpress': { - depends_on: [ 'tests-mysql' ], + depends_on: [ + 'tests-mysql', + config.env.development.objectCache && 'tests-object-cache', + ].filter( Boolean ), build: { context: '.', dockerfile: 'Tests-WordPress.Dockerfile', @@ -261,36 +267,40 @@ module.exports = function buildDockerComposeConfig( config ) { }, extra_hosts: [ 'host.docker.internal:host-gateway' ], }, - 'object-cache': { - depends_on: [ 'wordpress' ], - build: { - context: '.', - dockerfile: 'ObjectCache.Dockerfile', - args: imageBuildArgs, - }, - volumes: developmentMounts, - user: hostUser.fullUser, - environment: { - ...dbEnv.credentials, - ...dbEnv.development, - }, - extra_hosts: [ 'host.docker.internal:host-gateway' ], - }, - 'tests-object-cache': { - depends_on: [ 'tests-wordpress' ], - build: { - context: '.', - dockerfile: 'Tests-ObjectCache.Dockerfile', - args: imageBuildArgs, - }, - volumes: testsMounts, - user: hostUser.fullUser, - environment: { - ...dbEnv.credentials, - ...dbEnv.tests, - }, - extra_hosts: [ 'host.docker.internal:host-gateway' ], - }, + 'object-cache': config.env.development.objectCache + ? { + depends_on: [], + build: { + context: '.', + dockerfile: 'ObjectCache.Dockerfile', + args: imageBuildArgs, + }, + volumes: developmentMounts, + user: hostUser.fullUser, + environment: { + ...dbEnv.credentials, + ...dbEnv.development, + }, + extra_hosts: [ 'host.docker.internal:host-gateway' ], + } + : undefined, + 'tests-object-cache': config.env.tests.objectCache + ? { + depends_on: [], + build: { + context: '.', + dockerfile: 'Tests-ObjectCache.Dockerfile', + args: imageBuildArgs, + }, + volumes: testsMounts, + user: hostUser.fullUser, + environment: { + ...dbEnv.credentials, + ...dbEnv.tests, + }, + extra_hosts: [ 'host.docker.internal:host-gateway' ], + } + : undefined, }, volumes: { ...( ! config.env.development.coreSource && { wordpress: {} } ), diff --git a/packages/env/lib/config/test/__snapshots__/config-integration.js.snap b/packages/env/lib/config/test/__snapshots__/config-integration.js.snap index 53a2d652c740f..98b7b948606d2 100644 --- a/packages/env/lib/config/test/__snapshots__/config-integration.js.snap +++ b/packages/env/lib/config/test/__snapshots__/config-integration.js.snap @@ -29,6 +29,7 @@ exports[`Config Integration should load local and override configuration files 1 "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "objectCache": null, "phpVersion": null, "pluginSources": [], "port": 999, @@ -57,6 +58,7 @@ exports[`Config Integration should load local and override configuration files 1 "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "objectCache": null, "phpVersion": null, "pluginSources": [], "port": 456, @@ -102,6 +104,7 @@ exports[`Config Integration should load local configuration file 1`] = ` "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "objectCache": null, "phpVersion": null, "pluginSources": [], "port": 123, @@ -130,6 +133,7 @@ exports[`Config Integration should load local configuration file 1`] = ` "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "objectCache": null, "phpVersion": null, "pluginSources": [], "port": 8889, @@ -175,6 +179,7 @@ exports[`Config Integration should use default configuration 1`] = ` "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "objectCache": null, "phpVersion": null, "pluginSources": [], "port": 8888, @@ -203,6 +208,7 @@ exports[`Config Integration should use default configuration 1`] = ` "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "objectCache": null, "phpVersion": null, "pluginSources": [], "port": 8889, @@ -248,6 +254,7 @@ exports[`Config Integration should use environment variables over local and over "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "objectCache": null, "phpVersion": null, "pluginSources": [], "port": 12345, @@ -277,6 +284,7 @@ exports[`Config Integration should use environment variables over local and over "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "objectCache": null, "phpVersion": null, "pluginSources": [], "port": 61234, diff --git a/packages/env/lib/config/test/parse-config.js b/packages/env/lib/config/test/parse-config.js index ee808b973b16a..d27f6531d5fc1 100644 --- a/packages/env/lib/config/test/parse-config.js +++ b/packages/env/lib/config/test/parse-config.js @@ -46,6 +46,7 @@ const DEFAULT_CONFIG = { WP_SITEURL: 'http://localhost', WP_HOME: 'http://localhost', }, + objectCache: null, mappings: {}, lifecycleScripts: { afterStart: null, @@ -389,5 +390,32 @@ describe( 'parseConfig', () => { ); } } ); + + it( 'throws for invalid object-cache config option', async () => { + readRawConfigFile.mockImplementation( async ( configFile ) => { + if ( configFile === '/test/gutenberg/.wp-env.json' ) { + return { + objectCache: 'doesnotexist', + }; + } + + if ( configFile === '/test/gutenberg/.wp-env.override.json' ) { + return {}; + } + + throw new Error( 'Invalid File: ' + configFile ); + } ); + + expect.assertions( 1 ); + try { + await parseConfig( '/test/gutenberg', '/cache' ); + } catch ( error ) { + expect( error ).toEqual( + new ValidationError( + `Invalid /test/gutenberg/.wp-env.json: "objectCache" must be one of: "memcached".` + ) + ); + } + } ); } ); /* eslint-enable jest/no-conditional-expect */ diff --git a/packages/env/lib/config/validate-config.js b/packages/env/lib/config/validate-config.js index d80575dcab6e2..d18fce18c5e2d 100644 --- a/packages/env/lib/config/validate-config.js +++ b/packages/env/lib/config/validate-config.js @@ -40,11 +40,13 @@ function checkOneOfStrings( configFile, configKey, value, options ) { ); } + const validOptions = options + .map( ( option ) => `"${ option }"` ) + .join( ',' ); + if ( ! options.includes( value ) ) { throw new ValidationError( - `Invalid ${ configFile }: "${ configKey }" must be one of ${ options.join( - ',' - ) }` + `Invalid ${ configFile }: "${ configKey }" must be one of: ${ validOptions }.` ); } } diff --git a/packages/env/lib/test/build-docker-compose-config.js b/packages/env/lib/test/build-docker-compose-config.js index 95cf6419d5db4..6a0c2156edc96 100644 --- a/packages/env/lib/test/build-docker-compose-config.js +++ b/packages/env/lib/test/build-docker-compose-config.js @@ -131,4 +131,26 @@ describe( 'buildDockerComposeConfig', () => { expect( dockerConfig.volumes.wordpress ).toBe( undefined ); expect( dockerConfig.volumes[ 'tests-wordpress' ] ).toBe( undefined ); } ); + + it( 'should create "object-cache" and "tests-object-cache" entries', () => { + const envConfig = { + ...CONFIG, + objectCache: 'memcached', + }; + const dockerConfig = buildDockerComposeConfig( { + workDirectoryPath: '/path', + env: { development: envConfig, tests: envConfig }, + } ); + + expect( dockerConfig.services[ 'object-cache' ] ).not.toBe( undefined ); + expect( dockerConfig.services[ 'tests-object-cache' ] ).not.toBe( + undefined + ); + expect( dockerConfig.services.wordpress.depends_on ).toContain( + 'object-cache' + ); + expect( + dockerConfig.services[ 'tests-wordpress' ].depends_on + ).toContain( 'tests-object-cache' ); + } ); } ); From aeda54dcf363771ed1b23004dfea59d3cd9cdf22 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 10:27:45 +0100 Subject: [PATCH 11/21] prevent undefined --- .../env/lib/build-docker-compose-config.js | 90 ++++++++++--------- .../lib/test/build-docker-compose-config.js | 20 +++++ 2 files changed, 67 insertions(+), 43 deletions(-) diff --git a/packages/env/lib/build-docker-compose-config.js b/packages/env/lib/build-docker-compose-config.js index 27a8b802b06be..a2950748af64e 100644 --- a/packages/env/lib/build-docker-compose-config.js +++ b/packages/env/lib/build-docker-compose-config.js @@ -168,7 +168,7 @@ module.exports = function buildDockerComposeConfig( config ) { const developmentPorts = `\${WP_ENV_PORT:-${ config.env.development.port }}:80`; const testsPorts = `\${WP_ENV_TESTS_PORT:-${ config.env.tests.port }}:80`; - return { + const dockerComposeConfig = { version: '3.7', services: { mysql: { @@ -194,10 +194,7 @@ module.exports = function buildDockerComposeConfig( config ) { volumes: [ 'mysql-test:/var/lib/mysql' ], }, wordpress: { - depends_on: [ - 'mysql', - config.env.development.objectCache && 'object-cache', - ].filter( Boolean ), + depends_on: [ 'mysql' ], build: { context: '.', dockerfile: 'WordPress.Dockerfile', @@ -215,10 +212,7 @@ module.exports = function buildDockerComposeConfig( config ) { extra_hosts: [ 'host.docker.internal:host-gateway' ], }, 'tests-wordpress': { - depends_on: [ - 'tests-mysql', - config.env.development.objectCache && 'tests-object-cache', - ].filter( Boolean ), + depends_on: [ 'tests-mysql' ], build: { context: '.', dockerfile: 'Tests-WordPress.Dockerfile', @@ -267,40 +261,6 @@ module.exports = function buildDockerComposeConfig( config ) { }, extra_hosts: [ 'host.docker.internal:host-gateway' ], }, - 'object-cache': config.env.development.objectCache - ? { - depends_on: [], - build: { - context: '.', - dockerfile: 'ObjectCache.Dockerfile', - args: imageBuildArgs, - }, - volumes: developmentMounts, - user: hostUser.fullUser, - environment: { - ...dbEnv.credentials, - ...dbEnv.development, - }, - extra_hosts: [ 'host.docker.internal:host-gateway' ], - } - : undefined, - 'tests-object-cache': config.env.tests.objectCache - ? { - depends_on: [], - build: { - context: '.', - dockerfile: 'Tests-ObjectCache.Dockerfile', - args: imageBuildArgs, - }, - volumes: testsMounts, - user: hostUser.fullUser, - environment: { - ...dbEnv.credentials, - ...dbEnv.tests, - }, - extra_hosts: [ 'host.docker.internal:host-gateway' ], - } - : undefined, }, volumes: { ...( ! config.env.development.coreSource && { wordpress: {} } ), @@ -311,4 +271,48 @@ module.exports = function buildDockerComposeConfig( config ) { 'tests-user-home': {}, }, }; + + if ( config.env.development.objectCache ) { + dockerComposeConfig.services.wordpress.depends_on.push( + 'object-cache' + ); + dockerComposeConfig.services[ 'object-cache' ] = { + depends_on: [], + build: { + context: '.', + dockerfile: 'ObjectCache.Dockerfile', + args: imageBuildArgs, + }, + volumes: developmentMounts, + user: hostUser.fullUser, + environment: { + ...dbEnv.credentials, + ...dbEnv.development, + }, + extra_hosts: [ 'host.docker.internal:host-gateway' ], + }; + } + + if ( config.env.tests.objectCache ) { + dockerComposeConfig.services[ 'tests-wordpress' ].depends_on.push( + 'tests-object-cache' + ); + dockerComposeConfig.services[ 'tests-object-cache' ] = { + depends_on: [], + build: { + context: '.', + dockerfile: 'Tests-ObjectCache.Dockerfile', + args: imageBuildArgs, + }, + volumes: testsMounts, + user: hostUser.fullUser, + environment: { + ...dbEnv.credentials, + ...dbEnv.tests, + }, + extra_hosts: [ 'host.docker.internal:host-gateway' ], + }; + } + + return dockerComposeConfig; }; diff --git a/packages/env/lib/test/build-docker-compose-config.js b/packages/env/lib/test/build-docker-compose-config.js index 6a0c2156edc96..73a05b18daec1 100644 --- a/packages/env/lib/test/build-docker-compose-config.js +++ b/packages/env/lib/test/build-docker-compose-config.js @@ -132,6 +132,26 @@ describe( 'buildDockerComposeConfig', () => { expect( dockerConfig.volumes[ 'tests-wordpress' ] ).toBe( undefined ); } ); + it( 'should not create "object-cache" and "tests-object-cache" entries by default', () => { + const envConfig = { + ...CONFIG, + }; + const dockerConfig = buildDockerComposeConfig( { + workDirectoryPath: '/path', + env: { development: envConfig, tests: envConfig }, + } ); + + expect( dockerConfig.services[ 'object-cache' ] ).not.toBeDefined(); + expect( + dockerConfig.services[ 'tests-object-cache' ] + ).not.toBeDefined(); + expect( dockerConfig.services.wordpress.depends_on ).not.toContain( + 'object-cache' + ); + expect( + dockerConfig.services[ 'tests-wordpress' ].depends_on + ).not.toContain( 'tests-object-cache' ); + } ); it( 'should create "object-cache" and "tests-object-cache" entries', () => { const envConfig = { ...CONFIG, From fca697815eabb65d770d4dac8c3844eed81ba30c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 10:40:13 +0100 Subject: [PATCH 12/21] Ensure proper space --- packages/env/lib/init-config.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index 771749211ff79..ea68c36ebd6d9 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -248,7 +248,10 @@ RUN apt-get -qy install sudo RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; if ( 'memcached' === config.env[ env ].objectCache ) { - dockerFileContent += 'RUN apt-get -qy install libmemcached-dev'; + dockerFileContent += ` + +# Install Memcached +RUN apt-get -qy install libmemcached-dev`; } break; @@ -266,7 +269,10 @@ RUN apk --no-cache add sudo linux-headers RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; if ( 'memcached' === config.env[ env ].objectCache ) { - dockerFileContent += 'RUN apk add libmemcached-dev'; + dockerFileContent += ` + +# Install Memcached +RUN apk add libmemcached-dev`; } break; From a0e2cd04fc06ea543499acc46d6ac831da8fc419 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 10:50:01 +0100 Subject: [PATCH 13/21] Add another space issue --- packages/env/lib/init-config.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index ea68c36ebd6d9..61c5ea63c45fa 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -250,7 +250,7 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; if ( 'memcached' === config.env[ env ].objectCache ) { dockerFileContent += ` -# Install Memcached +# Install Memcached library RUN apt-get -qy install libmemcached-dev`; } @@ -271,7 +271,7 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; if ( 'memcached' === config.env[ env ].objectCache ) { dockerFileContent += ` -# Install Memcached +# Install Memcached library RUN apk add libmemcached-dev`; } @@ -288,7 +288,10 @@ RUN apk add libmemcached-dev`; ); if ( 'memcached' === config.env[ env ].objectCache ) { - dockerFileContent += 'RUN pecl install memcached-3.1.5'; + dockerFileContent += ` + +# Install Memcached PHP extension +RUN pecl install memcached-3.1.5`; } // Add better PHP settings. From 82dafeb7c23e46397da1b97c8e387b1f9e55bc45 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 13:08:02 +0100 Subject: [PATCH 14/21] Install zlib-dev --- packages/env/lib/init-config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index 61c5ea63c45fa..2a7af99a3f6d5 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -272,7 +272,7 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; dockerFileContent += ` # Install Memcached library -RUN apk add libmemcached-dev`; +RUN apk add zlib-dev libmemcached-dev`; } break; @@ -291,7 +291,7 @@ RUN apk add libmemcached-dev`; dockerFileContent += ` # Install Memcached PHP extension -RUN pecl install memcached-3.1.5`; +RUN pecl install memcached`; } // Add better PHP settings. From ee386a23d682438176accadbb8c58dde93dcfc37 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 13:25:15 +0100 Subject: [PATCH 15/21] Update commands --- packages/env/lib/init-config.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index 2a7af99a3f6d5..df0cd04f5da3d 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -251,7 +251,7 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; dockerFileContent += ` # Install Memcached library -RUN apt-get -qy install libmemcached-dev`; +RUN apt-get -qy install zlib-dev libmemcached-dev`; } break; @@ -291,7 +291,9 @@ RUN apk add zlib-dev libmemcached-dev`; dockerFileContent += ` # Install Memcached PHP extension -RUN pecl install memcached`; +RUN pecl install memcached +RUN docker-php-ext-enable memcached +`; } // Add better PHP settings. From fe18409f28d258c3da256df4dee6c955bb2feb72 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 13:41:22 +0100 Subject: [PATCH 16/21] zlib1g --- packages/env/lib/init-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index df0cd04f5da3d..179f1a5509170 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -251,7 +251,7 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; dockerFileContent += ` # Install Memcached library -RUN apt-get -qy install zlib-dev libmemcached-dev`; +RUN apt-get -qy install zlib1g-dev libmemcached-dev`; } break; From 15b6cc1b1602a1bd3db3ea459d4994e5c343224c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 13:27:28 +0000 Subject: [PATCH 17/21] Install right tools --- packages/env/lib/init-config.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index 179f1a5509170..8c16a603434a3 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -251,7 +251,7 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; dockerFileContent += ` # Install Memcached library -RUN apt-get -qy install zlib1g-dev libmemcached-dev`; +RUN apt-get -qy install zlib1g-dev libmemcached-dev memcached libmemcached-tools`; } break; @@ -272,7 +272,7 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; dockerFileContent += ` # Install Memcached library -RUN apk add zlib-dev libmemcached-dev`; +RUN apk add zlib-dev libmemcached-dev memcached`; } break; @@ -313,7 +313,6 @@ RUN rm /tmp/composer-setup.php`; dockerFileContent += ` USER $HOST_UID:$HOST_GID ENV PATH="\${PATH}:/home/$HOST_USERNAME/.composer/vendor/bin" -RUN composer global require --dev phpunit/phpunit:"^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" USER root`; return dockerFileContent; From 73f53e8c6c7df6ee11eda8d7c7e3f1d57aa63209 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 13:47:01 +0100 Subject: [PATCH 18/21] Update PHPCS config --- phpcs.xml.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 56cd6734e4f3e..610999a58c5f6 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -61,6 +61,7 @@ ./lib/compat/wordpress-*/html-api/*.php + ./packages/env/lib/object-cache/*.php From 3c33983544b383e4d28c594c0aedd214801a43a9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 14:44:44 +0100 Subject: [PATCH 19/21] Undo accidental change --- packages/env/lib/init-config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index 8c16a603434a3..8d10be76c4f14 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -313,6 +313,7 @@ RUN rm /tmp/composer-setup.php`; dockerFileContent += ` USER $HOST_UID:$HOST_GID ENV PATH="\${PATH}:/home/$HOST_USERNAME/.composer/vendor/bin" +RUN composer global require --dev phpunit/phpunit:"^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" USER root`; return dockerFileContent; From b9fd4dc77762276798cf4fc55665121095bee586 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 15:02:14 +0100 Subject: [PATCH 20/21] Update readme --- packages/env/README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/env/README.md b/packages/env/README.md index fb9e9751d9c66..44eba6ed16940 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -479,16 +479,17 @@ You can customize the WordPress installation, plugins and themes that the develo `.wp-env.json` supports fields for options applicable to both the tests and development instances. -| Field | Type | Default | Description | -| -------------- | -------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `"core"` | `string\|null` | `null` | The WordPress installation to use. If `null` is specified, `wp-env` will use the latest production release of WordPress. | -| `"phpVersion"` | `string\|null` | `null` | The PHP version to use. If `null` is specified, `wp-env` will use the default version used with production release of WordPress. | -| `"plugins"` | `string[]` | `[]` | A list of plugins to install and activate in the environment. | -| `"themes"` | `string[]` | `[]` | A list of themes to install in the environment. | -| `"port"` | `integer` | `8888` (`8889` for the tests instance) | The primary port number to use for the installation. You'll access the instance through the port: 'http://localhost:8888'. | -| `"testsPort"` | `integer` | `8889` | The port number for the test site. You'll access the instance through the port: 'http://localhost:8889'. | -| `"config"` | `Object` | See below. | Mapping of wp-config.php constants to their desired values. | -| `"mappings"` | `Object` | `"{}"` | Mapping of WordPress directories to local directories to be mounted in the WordPress instance. | +| Field | Type | Default | Description | +|-----------------| -------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `"core"` | `string\|null` | `null` | The WordPress installation to use. If `null` is specified, `wp-env` will use the latest production release of WordPress. | +| `"phpVersion"` | `string\|null` | `null` | The PHP version to use. If `null` is specified, `wp-env` will use the default version used with production release of WordPress. | +| `"plugins"` | `string[]` | `[]` | A list of plugins to install and activate in the environment. | +| `"themes"` | `string[]` | `[]` | A list of themes to install in the environment. | +| `"port"` | `integer` | `8888` (`8889` for the tests instance) | The primary port number to use for the installation. You'll access the instance through the port: 'http://localhost:8888'. | +| `"testsPort"` | `integer` | `8889` | The port number for the test site. You'll access the instance through the port: 'http://localhost:8889'. | +| `"config"` | `Object` | See below. | Mapping of wp-config.php constants to their desired values. | +| `"mappings"` | `Object` | `"{}"` | Mapping of WordPress directories to local directories to be mounted in the WordPress instance. | +| `"objectCache"` | `string` | `null` | Object cache to set up. Currently only supports `"memcached"` | _Note: the port number environment variables (`WP_ENV_PORT` and `WP_ENV_TESTS_PORT`) take precedent over the .wp-env.json values._ From 9e552f977a9be932902ff7e001a7cf81c0197ebc Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Nov 2023 15:18:00 +0100 Subject: [PATCH 21/21] Install `libssl-dev` --- packages/env/lib/init-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index 8d10be76c4f14..20026f9a524d0 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -251,7 +251,7 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; dockerFileContent += ` # Install Memcached library -RUN apt-get -qy install zlib1g-dev libmemcached-dev memcached libmemcached-tools`; +RUN apt-get -qy install zlib1g-dev libmemcached-dev memcached libmemcached-tools libssl-dev`; } break;