diff --git a/.ddev/commands/playwright/playwright b/.ddev/commands/playwright/playwright new file mode 100755 index 0000000..b2e9dcc --- /dev/null +++ b/.ddev/commands/playwright/playwright @@ -0,0 +1,14 @@ +#!/bin/bash +#ddev-generated +# Remove the line above if you don't want this file to be overwritten when you run +# ddev get julienloizelet/ddev-playwright +# +# This file comes from https://github.com/julienloizelet/ddev-playwright +# +cd /var/www/html || exit 1 +cd "${PLAYWRIGHT_TEST_DIR}" || exit 1 + +export PLAYWRIGHT_BROWSERS_PATH=0 +PRE="sudo -u pwuser PLAYWRIGHT_BROWSERS_PATH=0 " + +$PRE yarn playwright "$@" diff --git a/.ddev/commands/playwright/playwright-install b/.ddev/commands/playwright/playwright-install new file mode 100755 index 0000000..b4165b9 --- /dev/null +++ b/.ddev/commands/playwright/playwright-install @@ -0,0 +1,17 @@ +#!/bin/bash +#ddev-generated +# Remove the line above if you don't want this file to be overwritten when you run +# ddev get julienloizelet/ddev-playwright +# +# This file comes from https://github.com/julienloizelet/ddev-playwright +# +cd /var/www/html || exit 1 +cd "${PLAYWRIGHT_TEST_DIR}" || exit 1 + +export PLAYWRIGHT_BROWSERS_PATH=0 +PRE="sudo -u pwuser PLAYWRIGHT_BROWSERS_PATH=0 " + +$PRE yarn install +$PRE yarn playwright install --with-deps +# Conditionally copy an .env file if an example file exists +[ -f .env.example ] && [ ! -f .env ] && $PRE cp -n .env.example .env; exit 0 diff --git a/.ddev/commands/web/orchestrate b/.ddev/commands/web/orchestrate index a7e0865..cc9a96f 100755 --- a/.ddev/commands/web/orchestrate +++ b/.ddev/commands/web/orchestrate @@ -5,7 +5,8 @@ mkdir -p "${DDEV_DOCROOT}" pushd "${DDEV_DOCROOT}" -PLUGIN_FOLDER="${DDEV_DOCROOT}/wp-content/mu-plugins/${PLUGIN_NAME:-$DDEV_PROJECT}" +MUPLUGIN_FOLDER="${DDEV_DOCROOT}/wp-content/mu-plugins/${PLUGIN_NAME:-$DDEV_PROJECT}" +TEST_PLUGIN_FOLDER="${DDEV_DOCROOT}/wp-content/plugins/wp-stash-test-plugin" VALID_ARGS=$(getopt -o fp: --long force,plugin: -- "$@") if [[ $? -ne 0 ]]; then exit 1; @@ -19,7 +20,7 @@ while [ : ]; do shift export RECREATE_ENV=1; popd - find "${DDEV_DOCROOT}" -mindepth 1 ! -regex "^${PLUGIN_FOLDER}\(/.*\)?" -delete + find "${DDEV_DOCROOT}" -mindepth 1 ! -regex "^${MUPLUGIN_FOLDER}\(/.*\)?" ! -regex "^${TEST_PLUGIN_FOLDER}\(/.*\)?" -delete pushd "${DDEV_DOCROOT}" ;; --) shift; diff --git a/.ddev/commands/web/orchestrate.d/35_activate_test_plugin.sh b/.ddev/commands/web/orchestrate.d/35_activate_test_plugin.sh new file mode 100644 index 0000000..574ae8a --- /dev/null +++ b/.ddev/commands/web/orchestrate.d/35_activate_test_plugin.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +pushd "${DDEV_DOCROOT}" || exit + +flags="" +if [ "${WP_MULTISITE}" = "true" ]; then + flags+=" --network" +fi + +wp plugin activate wp-stash-test-plugin $flags + +popd diff --git a/.ddev/config.yaml b/.ddev/config.yaml index f5d52c7..4260f1b 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -5,7 +5,7 @@ php_version: "8.0" webserver_type: nginx-fpm router_http_port: "80" router_https_port: "443" -xdebug_enabled: true +xdebug_enabled: false additional_hostnames: [] additional_fqdns: [] database: diff --git a/.ddev/docker-compose.playwright.yaml b/.ddev/docker-compose.playwright.yaml new file mode 100644 index 0000000..05906fa --- /dev/null +++ b/.ddev/docker-compose.playwright.yaml @@ -0,0 +1,38 @@ +#ddev-generated +# Remove the line above if you don't want this file to be overwritten when you run +# ddev get julienloizelet/ddev-playwright +# +# This file comes from https://github.com/julienloizelet/ddev-playwright +# +services: + playwright: + build: + context: playwright-build + container_name: ddev-${DDEV_SITENAME}-playwright + hostname: ${DDEV_SITENAME}-playwright + # These labels ensure this service is discoverable by ddev. + labels: + com.ddev.site-name: ${DDEV_SITENAME} + com.ddev.approot: $DDEV_APPROOT + environment: + # Modify the PLAYWRIGHT_TEST_DIR folder path to suit your needs + - PLAYWRIGHT_TEST_DIR=tests/Playwright + - NETWORK_IFACE=eth0 + - DISPLAY=:1 + - VIRTUAL_HOST=$DDEV_HOSTNAME + - HTTP_EXPOSE=8443:8444,9322:9323 + - HTTPS_EXPOSE=8444:8444,9323:9323 + - DDEV_UID=${DDEV_UID} + - DDEV_GID=${DDEV_GID} + expose: + - "8444" + - "9323" + depends_on: + - web + volumes: + - .:/mnt/ddev_config + - ddev-global-cache:/mnt/ddev-global-cache + - ../:/var/www/html:rw + external_links: + - ddev-router:${DDEV_HOSTNAME} + working_dir: /var/www/html diff --git a/.ddev/playwright-build/Dockerfile b/.ddev/playwright-build/Dockerfile new file mode 100644 index 0000000..33c3b5b --- /dev/null +++ b/.ddev/playwright-build/Dockerfile @@ -0,0 +1,57 @@ +#ddev-generated +# Remove the line above if you don't want this file to be overwritten when you run +# ddev get julienloizelet/ddev-playwright +# +# This file comes from https://github.com/julienloizelet/ddev-playwright +# +# If on arm64 machine, edit to use mcr.microsoft.com/playwright:focal-arm64 +FROM mcr.microsoft.com/playwright:focal + +# Debian images by default disable apt caching, so turn it on until we finish +# the build. +RUN mv /etc/apt/apt.conf.d/docker-clean /etc/apt/docker-clean-disabled + +USER root + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update \ + && apt-get install -y sudo libnss3-tools + +# Give the pwuser user full `sudo` privileges +RUN echo "pwuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/pwuser \ + && chmod 0440 /etc/sudoers.d/pwuser + +# CAROOT for `mkcert` to use, has the CA config +ENV CAROOT=/mnt/ddev-global-cache/mkcert + +# Install the correct architecture binary of `mkcert` +RUN export TARGETPLATFORM=linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') && mkdir -p /usr/local/bin && curl --fail -JL -s -o /usr/local/bin/mkcert "https://dl.filippo.io/mkcert/latest?for=${TARGETPLATFORM}" +RUN chmod +x /usr/local/bin/mkcert + + +# Install a window manager. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update \ + && apt-get install -y icewm xauth + +# Install kasmvnc for remote access. +RUN /bin/bash -c 'if [ $(arch) == "aarch64" ]; then KASM_ARCH=arm64; else KASM_ARCH=amd64; fi; wget https://github.com/kasmtech/KasmVNC/releases/download/v1.1.0/kasmvncserver_bullseye_1.1.0_${KASM_ARCH}.deb' +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get install -y ./kasmvncserver*.deb + +# We're done with apt so disable caching again for the final image. +RUN mv /etc/apt/docker-clean-disabled /etc/apt/apt.conf.d/docker-clean + +# prepare KasmVNC +RUN sudo -u pwuser mkdir /home/pwuser/.vnc +COPY kasmvnc.yaml xstartup /home/pwuser/.vnc/ +RUN chown pwuser:pwuser /home/pwuser/.vnc/* +RUN sudo -u pwuser touch /home/pwuser/.vnc/.de-was-selected +RUN sudo -u pwuser /bin/bash -c 'echo -e "secret\nsecret\n" | kasmvncpasswd -wo -u pwuser' # We actually disable auth, but KASM complains without it + + +COPY entrypoint.sh /root/entrypoint.sh +ENTRYPOINT "/root/entrypoint.sh" diff --git a/.ddev/playwright-build/entrypoint.sh b/.ddev/playwright-build/entrypoint.sh new file mode 100755 index 0000000..00d9bd5 --- /dev/null +++ b/.ddev/playwright-build/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/bash +#ddev-generated +# Remove the line above if you don't want this file to be overwritten when you run +# ddev get julienloizelet/ddev-playwright +# +# This file comes from https://github.com/julienloizelet/ddev-playwright +# + +# Change pwuser IDs to the host IDs supplied by DDEV +usermod -u ${DDEV_UID} pwuser +groupmod -g ${DDEV_GID} pwuser +usermod -a -G ssl-cert pwuser + +# Install DDEV certificate +sudo -u pwuser mkcert -install + +# Run CMD from parameters as pwuser +sudo -u pwuser vncserver -fg -disableBasicAuth diff --git a/.ddev/playwright-build/kasmvnc.yaml b/.ddev/playwright-build/kasmvnc.yaml new file mode 100644 index 0000000..c42c849 --- /dev/null +++ b/.ddev/playwright-build/kasmvnc.yaml @@ -0,0 +1,14 @@ +#ddev-generated +# Remove the line above if you don't want this file to be overwritten when you run +# ddev get julienloizelet/ddev-playwright +# +# This file comes from https://github.com/julienloizelet/ddev-playwright +# +logging: + log_writer_name: all + log_dest: syslog + level: 100 + +network: + ssl: + require_ssl: false diff --git a/.ddev/playwright-build/xstartup b/.ddev/playwright-build/xstartup new file mode 100755 index 0000000..7d5896d --- /dev/null +++ b/.ddev/playwright-build/xstartup @@ -0,0 +1,32 @@ +#!/bin/sh +#ddev-generated +# Remove the line above if you don't want this file to be overwritten when you run +# ddev get julienloizelet/ddev-playwright +# +# This file comes from https://github.com/julienloizelet/ddev-playwright +# + +export DISPLAY=:1 + +unset SESSION_MANAGER +unset DBUS_SESSION_BUS_ADDRESS +OS=`uname -s` +if [ $OS = 'Linux' ]; then + case "$WINDOWMANAGER" in + *gnome*) + if [ -e /etc/SuSE-release ]; then + PATH=$PATH:/opt/gnome/bin + export PATH + fi + ;; + esac +fi +if [ -x /etc/X11/xinit/xinitrc ]; then + exec /etc/X11/xinit/xinitrc +fi +if [ -f /etc/X11/xinit/xinitrc ]; then + exec sh /etc/X11/xinit/xinitrc +fi +[ -r $HOME/.Xresources ] && xrdb $HOME/.Xresources +xterm -geometry 80x24+10+10 -ls -title "$VNCDESKTOP Desktop" & +icewm-session diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..2f15cb3 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,13 @@ +name: Run Playwright tests via DDEV +on: + push: + workflow_dispatch: +jobs: + ddev-playwright: + uses: inpsyde/reusable-workflows/.github/workflows/ddev-playwright.yml@feature/ddev-playwright + secrets: + COMPOSER_AUTH_JSON: ${{ secrets.PACKAGIST_AUTH_JSON }} + with: + DDEV_ORCHESTRATE_CMD: ddev orchestrate + PLAYWRIGHT_INSTALL_CMD: ddev playwright-install + PLAYWRIGHT_RUN_CMD: ddev playwright test diff --git a/.idea/WP-Stash.iml b/.idea/WP-Stash.iml index bbc8f93..0dc48bc 100644 --- a/.idea/WP-Stash.iml +++ b/.idea/WP-Stash.iml @@ -6,6 +6,7 @@ + @@ -50,6 +51,11 @@ + + + + + diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index f8d235c..68ae064 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -1,13 +1,13 @@ - + mariadb true true DDEV generated data source org.mariadb.jdbc.Driver - jdbc:mariadb://127.0.0.1:32954/db?user=db&password=db + jdbc:mariadb://127.0.0.1:32805/db?user=db&password=db $ProjectFileDir$ diff --git a/.idea/php.xml b/.idea/php.xml index 9221fa7..217cf55 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -69,6 +69,11 @@ + + + + + diff --git a/.idea/phpunit.xml b/.idea/phpunit.xml index 4f8104c..3ae69d7 100644 --- a/.idea/phpunit.xml +++ b/.idea/phpunit.xml @@ -3,6 +3,8 @@ diff --git a/dropin/object-cache.php b/dropin/object-cache.php index f6d1ff0..1359ab9 100644 --- a/dropin/object-cache.php +++ b/dropin/object-cache.php @@ -46,6 +46,28 @@ function wp_cache_add($key, $data, $group = '', $expire = 0) return $wp_object_cache->add($key, $data, $group, (int) $expire); } +/** + * Adds multiple values to the cache in one call. + * + * @since 6.0.0 + * + * @see WP_Object_Cache::add_multiple() + * @global WP_Object_Cache $wp_object_cache Object cache global instance. + * + * @param array $data Array of keys and values to be set. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param int $expire 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 $data, $group = '', $expire = 0 ) { + global $wp_object_cache; + assert($wp_object_cache instanceof ObjectCacheProxy); + + return $wp_object_cache->add_multiple( $data, $group, $expire ); +} + /** * Closes the cache. * @@ -170,7 +192,7 @@ function wp_cache_incr($key, $offset = 1, $group = '') */ function wp_cache_init() { - $autoloadFile = __DIR__.'/../vendor/autoload.php'; + $autoloadFile = __DIR__ . '/../vendor/autoload.php'; if (file_exists($autoloadFile)) { require_once $autoloadFile; } @@ -287,11 +309,6 @@ function wp_cache_add_non_persistent_groups($groups) function wp_cache_reset() { _deprecated_function(__FUNCTION__, '3.5'); - - global $wp_object_cache; - assert($wp_object_cache instanceof ObjectCacheProxy); - - return $wp_object_cache->reset(); } /** @@ -314,3 +331,107 @@ function wp_cache_get_multiple($keys, $group = '', $force = false) return $wp_object_cache->get_multiple($keys, $group, $force); } + +/** + * Sets multiple values to the cache in one call. + * + * @since 6.0.0 + * + * @see WP_Object_Cache::set_multiple() + * @global WP_Object_Cache $wp_object_cache Object cache global instance. + * + * @param array $data Array of keys and values to be set. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param int $expire 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 $data, $group = '', $expire = 0 ) { + global $wp_object_cache; + assert($wp_object_cache instanceof ObjectCacheProxy); + + return $wp_object_cache->set_multiple($data, $group, $expire); +} + +/** + * Deletes multiple values from the cache in one call. + * + * @since 6.0.0 + * + * @see WP_Object_Cache::delete_multiple() + * @global WP_Object_Cache $wp_object_cache Object cache global instance. + * + * @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; + assert($wp_object_cache instanceof ObjectCacheProxy); + + return $wp_object_cache->delete_multiple( $keys, $group ); +} + +/** + * Removes all cache items from the in-memory runtime cache. + * + * @since 6.0.0 + * + * @see WP_Object_Cache::flush() + * + * @return bool True on success, false on failure. + */ +function wp_cache_flush_runtime() { + global $wp_object_cache; + assert($wp_object_cache instanceof ObjectCacheProxy); + return $wp_object_cache->flush_runtime(); +} + +/** + * Removes all cache items in a group, if the object cache implementation supports it. + * + * Before calling this function, always check for group flushing support using the + * `wp_cache_supports( 'flush_group' )` function. + * + * @since 6.1.0 + * + * @see WP_Object_Cache::flush_group() + * @global WP_Object_Cache $wp_object_cache Object cache global instance. + * + * @param string $group Name of group to remove from cache. + * @return bool True if group was flushed, false otherwise. + */ +function wp_cache_flush_group( $group ) { + global $wp_object_cache; + assert($wp_object_cache instanceof ObjectCacheProxy); + return $wp_object_cache->flush_group($group); +} + +/** + * 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): bool +{ + + switch ($feature) { + case 'add_multiple': + case 'set_multiple': + case 'get_multiple': + case 'delete_multiple': + case 'flush_runtime': + case 'flush_group': + return true; + + default: + return false; + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 10495d1..9a0e5f7 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -1,5 +1,6 @@ + ./src - \ No newline at end of file + diff --git a/src/Generator/CacheKeyGenerator.php b/src/Generator/CacheKeyGenerator.php index 906ffe0..6b164e7 100644 --- a/src/Generator/CacheKeyGenerator.php +++ b/src/Generator/CacheKeyGenerator.php @@ -1,6 +1,5 @@ globalGroups[$group])) { $parts[] = $this->blogId; diff --git a/src/ObjectCacheProxy.php b/src/ObjectCacheProxy.php index 6c8514c..1f4d65a 100644 --- a/src/ObjectCacheProxy.php +++ b/src/ObjectCacheProxy.php @@ -7,6 +7,7 @@ namespace Inpsyde\WpStash; use Inpsyde\WpStash\Generator\KeyGen; +use Inpsyde\WpStash\Stash\PersistenceAwareComposite; // because WordPress... // phpcs:disable @@ -91,18 +92,18 @@ class ObjectCacheProxy /** * Sets up object properties * - * @since WP 2.0.8 - * * @param StashAdapter $non_persistent * @param StashAdapter $persistent * @param KeyGen $key_gen + * + * @since WP 2.0.8 + * */ public function __construct( StashAdapter $non_persistent, StashAdapter $persistent, KeyGen $key_gen ) { - $this->non_persistent = $non_persistent; $this->persistent = $persistent; $this->key_gen = $key_gen; @@ -111,12 +112,12 @@ public function __construct( /** * Make private properties readable for backwards compatibility. * - * @since WP 4.0.0 - * @access public - * * @param string $name Property to get. * * @return mixed Property. + * @since WP 4.0.0 + * @access public + * */ public function __get($name) { @@ -126,13 +127,13 @@ public function __get($name) /** * Make private properties settable for backwards compatibility. * - * @since WP 4.0.0 - * @access public - * * @param string $name Property to set. * @param mixed $value Property value. * * @return mixed Newly-set property. + * @since WP 4.0.0 + * @access public + * */ public function __set($name, $value) { @@ -142,12 +143,12 @@ public function __set($name, $value) /** * Make private properties checkable for backwards compatibility. * - * @since WP 4.0.0 - * @access public - * * @param string $name Property to check if set. * * @return bool Whether the property is set. + * @since WP 4.0.0 + * @access public + * */ public function __isset($name) { @@ -157,10 +158,11 @@ public function __isset($name) /** * Make private properties un-settable for backwards compatibility. * + * @param string $name Property to unset. + * * @since WP 4.0.0 * @access public * - * @param string $name Property to unset. */ public function __unset($name) { @@ -170,18 +172,18 @@ public function __unset($name) /** * Adds data to the cache if it doesn't already exist. * - * @uses WP_Object_Cache::_exists Checks to see if the cache already has data. - * @uses WP_Object_Cache::set Sets the data after the checking the cache - * contents existence. - * - * @since WP 2.0.0 - * * @param int|string $key What to call the contents in the cache * @param mixed $data The contents to store in the cache * @param string $group Where to group the cache contents * @param int $expire When to expire the cache contents * * @return bool False if cache key and group already exist, true on success + * @since WP 2.0.0 + * + * @uses WP_Object_Cache::_exists Checks to see if the cache already has data. + * @uses WP_Object_Cache::set Sets the data after the checking the cache + * contents existence. + * */ public function add($key, $data, $group = 'default', $expire = 0) { @@ -189,12 +191,39 @@ public function add($key, $data, $group = 'default', $expire = 0) return false; } - $cache_key = $this->key_gen->create((string) $key, (string) $group); + $cache_key = $this->key_gen->create((string)$key, (string)$group); return $this->choose_pool($group) ->add($cache_key, $data, $expire); } + /** + * Adds multiple values to the cache in one call. + * + * @param array $data Array of keys and values to be added. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param int $expire 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. + * @since 6.0.0 + * + */ + public function add_multiple(array $data, string $group, int $expire) + { + if (wp_suspend_cache_addition()) { + return array_fill_keys(array_keys($data), false); + } + $originalKeys = array_keys($data); + $data = $this->transform_keys_for_group($data, $group); + + return array_combine( + $originalKeys, + $this->choose_pool($group)->addMultiple($data, $expire) + ); + } + /** * @param $group * @@ -218,7 +247,7 @@ private function choose_pool($group): StashAdapter */ public function add_global_groups($groups): bool { - if (! $this->key_gen instanceof Generator\MultisiteKeyGen) { + if (!$this->key_gen instanceof Generator\MultisiteKeyGen) { return false; } $this->key_gen->addGlobalGroups($groups); @@ -229,15 +258,15 @@ public function add_global_groups($groups): bool /** * Sets the list of non persistent groups. * - * @since WP 2.6.0 - * * @param array $groups List of non persistent groups. * * @return array + * @since WP 2.6.0 + * */ public function add_non_persistent_groups($groups): array { - $groups = (array) $groups; + $groups = (array)$groups; $groups = array_fill_keys($groups, true); $this->non_persistent_groups = array_merge($this->non_persistent_groups, $groups); @@ -248,13 +277,13 @@ public function add_non_persistent_groups($groups): array /** * Decrement numeric cache item's value * - * @since WP 3.3.0 - * * @param int|string $key The cache key to increment * @param int $offset The amount by which to decrement the item's value. Default is 1. * @param string $group The group the key is in. * * @return false|int False on failure, the item's new value on success. + * @since WP 3.3.0 + * */ public function decr($key, $offset = 1, $group = 'default') { @@ -267,16 +296,16 @@ public function decr($key, $offset = 1, $group = 'default') * * If the cache key does not exist in the group, then nothing will happen. * - * @since WP 2.0.0 - * * @param int|string $key What the contents in the cache are called * @param string $group Where the cache contents are grouped * * @return bool False if the contents weren't deleted and true on success + * @since WP 2.0.0 + * */ public function delete($key, $group = 'default'): bool { - $cache_key = $this->key_gen->create((string) $key, (string) $group); + $cache_key = $this->key_gen->create((string)$key, (string)$group); return $this->choose_pool($group) ->delete($cache_key); @@ -285,9 +314,9 @@ public function delete($key, $group = 'default'): bool /** * Clears the object cache of all data * + * @return bool Always returns true * @since WP 2.0.0 * - * @return bool Always returns true */ public function flush(): bool { @@ -304,25 +333,23 @@ public function flush(): bool */ public function purge(): bool { - return $this->persistent->purge() && $this->non_persistent->purge(); } /** * Increment numeric cache item's value * - * @since WP 3.3.0 - * * @param int|string $key The cache key to increment * @param int $offset The amount by which to increment the item's value. Default is 1. * @param string $group The group the key is in. * * @return false|int False on failure, the item's new value on success. + * @since WP 3.3.0 */ public function incr($key, $offset = 1, $group = 'default') { $data = $this->get($key, $group); - if (! $data || ! is_numeric($data)) { + if (!$data || !is_numeric($data)) { return false; } @@ -338,18 +365,18 @@ public function incr($key, $offset = 1, $group = 'default') * * On failure, the number of cache misses will be incremented. * - * @since WP 2.0.0 - * * @param int|string $key What the contents in the cache are called * @param string $group Where the cache contents are grouped * @param bool $force Whether to force a refetch rather than relying on the local cache (default is false) * * @return bool|mixed False on failure to retrieve contents or the cache * contents on success + * @since WP 2.0.0 + * */ public function get($key, $group = 'default', $force = false, &$found = null) { - $cache_key = $this->key_gen->create((string) $key, (string) $group); + $cache_key = $this->key_gen->create((string)$key, (string)$group); $result = $this->choose_pool($group) ->get($cache_key); @@ -372,18 +399,18 @@ public function get($key, $group = 'default', $force = false, &$found = null) * expire for each time a page is accessed and PHP finishes. The method is * more for cache plugins which use files. * - * @since WP 2.0.0 - * * @param int|string $key What to call the contents in the cache * @param mixed $data The contents to store in the cache * @param string $group Where to group the cache contents * @param int $expire Not Used * * @return bool Always returns true + * @since WP 2.0.0 + * */ public function set($key, $data, $group = 'default', $expire = 0) { - $cache_key = $this->key_gen->create((string) $key, (string) $group); + $cache_key = $this->key_gen->create((string)$key, (string)$group); return $this->choose_pool($group) ->set($cache_key, $data, $expire); @@ -392,19 +419,19 @@ public function set($key, $data, $group = 'default', $expire = 0) /** * Replace the contents in the cache, if contents already exist * - * @since WP 2.0.0 - * @see WP_Object_Cache::set() - * * @param int|string $key What to call the contents in the cache * @param mixed $data The contents to store in the cache * @param string $group Where to group the cache contents * @param int $expire When to expire the cache contents * * @return bool False if not exists, true if contents were replaced + * @see WP_Object_Cache::set() + * + * @since WP 2.0.0 */ public function replace($key, $data, $group = 'default', $expire = 0) { - $cache_key = $this->key_gen->create((string) $key, (string) $group); + $cache_key = $this->key_gen->create((string)$key, (string)$group); return $this->choose_pool($group) ->replace($cache_key, $data, $expire); @@ -438,38 +465,143 @@ public function stats() } /** - * Switch the interal blog id. + * Switch the internal blog id. * * This changes the blog id used to create keys in blog specific groups. * + * @param int $blog_id Blog ID + * * @since WP 3.5.0 * - * @param int $blog_id Blog ID */ public function switch_to_blog($blog_id) { - if (! ($this->key_gen instanceof Generator\MultisiteKeyGen)) { + if (!($this->key_gen instanceof Generator\MultisiteKeyGen)) { return; } - $this->key_gen->switchToBlog((int) $blog_id); + $this->key_gen->switchToBlog((int)$blog_id); } /** - * @param $keys + * @param array $keys * @param string $group - * @param false $force + * @param bool $force + * * @return array */ - public function get_multiple($keys, $group = '', $force = false) + public function get_multiple(array $keys, string $group = '', bool $force = false): array { $keys = array_unique($keys); $cache_keys = []; foreach ($keys as $key) { - $cache_keys[] = $this->key_gen->create((string) $key, (string) $group); + $cache_keys[] = $this->key_gen->create((string)$key, (string)$group); } $items = $this->choose_pool($group)->getMultiple($cache_keys); return array_combine($keys, $items); } + + /** + * @return bool + */ + public function flush_runtime(): bool + { + $this->non_persistent->clear(); + $this->persistent->clearNonPersistent(); + + return true; + } + + /** + * @param string $group + * + * @return bool + */ + public function flush_group(string $group): bool + { + $this->choose_pool($group)->clear(); + + return true; + } + + /** + * Sets multiple values to the cache in one call. + * + * @since 6.0.0 + * + * @param array $data Array of key and value to be set. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param int $expire 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 always true. + */ + public function set_multiple(array $data, string $group, int $expire): array + { + $originalKeys = array_keys($data); + $data = $this->transform_keys_for_group($data, $group); + $pool = $this->choose_pool($group); + + return array_combine($originalKeys, $pool->setMultiple($data, $expire)); + } + + /** + * Runs our cache key generator across all entries + * + * @param array $data + * @param string $group + * + * @return array + */ + private function transform_keys_for_group(array $data, string $group): array + { + return $this->array_map_key( + function ($key) use ($group) { + return $this->key_gen->create((string)$key, $group); + }, + $data + ); + } + + /** + * Changes array keys based on a callback + * @param $callback + * @param $array + * + * @return mixed + */ + private function array_map_key($callback, $array) + { + $result = []; + array_walk( + $array, + function ($val, $key) use ($callback,&$result) { + $result[$callback($key, $val)] = $val; + } + ); + + return $result; + } + + /** + * Deletes multiple values from the cache in one call. + * + * @since 6.0.0 + * + * @param array $keys Array of keys to be 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 delete_multiple(array $keys, string $group) + { + $keys = array_unique($keys); + $cache_keys = []; + foreach ($keys as $key) { + $cache_keys[] = $this->key_gen->create((string)$key, (string)$group); + } + + $items = $this->choose_pool($group)->deleteMultiple($cache_keys); + return array_combine($keys, $items); + } } diff --git a/src/Stash/PersistenceAwareComposite.php b/src/Stash/PersistenceAwareComposite.php new file mode 100644 index 0000000..5ddb827 --- /dev/null +++ b/src/Stash/PersistenceAwareComposite.php @@ -0,0 +1,25 @@ +drivers as $driver) { + if (!$driver->isPersistent()) { + $driver->clear(); + } + } + } +} diff --git a/src/StashAdapter.php b/src/StashAdapter.php index 6d093ed..bac09f4 100644 --- a/src/StashAdapter.php +++ b/src/StashAdapter.php @@ -5,12 +5,15 @@ namespace Inpsyde\WpStash; +use Inpsyde\WpStash\Generator\KeyGen; +use Inpsyde\WpStash\Stash\PersistenceAwareComposite; use Stash\Interfaces\ItemInterface; use Stash\Invalidation; use Stash\Pool; // phpcs:disable Inpsyde.CodeQuality.VariablesName.SnakeCaseVar // phpcs:disable Inpsyde.CodeQuality.ForbiddenPublicProperty.Found +// phpcs:disable Inpsyde.CodeQuality.NoAccessors.NoSetter /** * Class StashAdapter @@ -68,6 +71,42 @@ public function add(string $key, $data, int $expire = 0): bool return $this->set($key, $data, $expire); } + /** + * Sets multiple items in one call if they do not yet exist + * + * @param array $data + * @param int $expire + * + * @return array + */ + public function addMultiple(array $data, int $expire = 0): array + { + $result = []; + $keys = array_keys($data); + foreach ($this->pool->getItems($keys) as $item) { + $key = $item->getKey(); + $wpCacheKey = '/' . $key; // Item swallows our first slash with implode + if ($this->pool->hasItem($key)) { + $result[$wpCacheKey] = false; + continue; + } + /** + * @var ItemInterface $item + */ + $item->set($data[$wpCacheKey]); + if ($expire) { + $item->expiresAfter($expire); + } + + $item->setInvalidationMethod(Invalidation::OLD); + $this->pool->saveDeferred($item); + + $result[$key] = true; + } + + return $result; + } + /** * Set/update a cache item. * @@ -145,10 +184,41 @@ public function getMultiple(array $keys): array { $result = []; foreach ($this->pool->getItems($keys) as $item) { + $key = $item->getKey(); + $wpCacheKey = '/' . $key; // Item swallows our first slash with implode /** * @var ItemInterface $item */ - $result[$item->getKey()] = $this->getValueFromItem($item); + $result[$wpCacheKey] = $this->getValueFromItem($item); + } + + return $result; + } + + /** + * @param array $data + * @param int $expire + * + * @return array + */ + public function setMultiple(array $data, int $expire = 0): array + { + $result = []; + $keys = array_keys($data); + foreach ($this->pool->getItems($keys) as $item) { + $key = $item->getKey(); + $wpCacheKey = '/' . $key; // Item swallows our first slash with implode + /** + * @var ItemInterface $item + */ + $item->set($data[$wpCacheKey]); + if ($expire) { + $item->expiresAfter($expire); + } + + $item->setInvalidationMethod(Invalidation::OLD); + $this->pool->saveDeferred($item); + $result[$wpCacheKey] = true; } return $result; @@ -246,4 +316,34 @@ public function __destruct() { $this->pool->commit(); } + + public function deleteMultiple(array $cache_keys): array + { + $result = []; + /** + * Pool::deleteItems() unfortunately does not provide the required metadata + */ + foreach ($this->pool->getItems($cache_keys) as $item) { + /** + * @var ItemInterface $item + */ + $result[$item->getKey()] = $item->clear(); + } + + return $result; + } + + /** + * It would be good to be able to do this closer to the Stash API in the future. + * For now, there is no other way to access only the non-persistent drivers of a composite. + * @return void + */ + public function clearNonPersistent(): void + { + $driver = $this->pool->getDriver(); + if (!$driver instanceof PersistenceAwareComposite) { + return; + } + $driver->clearNonPersistent(); + } } diff --git a/src/WpStash.php b/src/WpStash.php index 7000b7b..2fdf62f 100644 --- a/src/WpStash.php +++ b/src/WpStash.php @@ -7,6 +7,7 @@ namespace Inpsyde\WpStash; use Inpsyde\WpStash\Generator\KeyGen; +use Inpsyde\WpStash\Stash\PersistenceAwareComposite; use Stash\Driver\Composite; use Stash\Driver\Ephemeral; use Stash\Interfaces\DriverInterface; @@ -130,7 +131,7 @@ public function driver(): DriverInterface && !$driver instanceof Composite && !$driver instanceof Ephemeral ) { - $driver = new Composite( + $driver = new PersistenceAwareComposite( [ 'drivers' => [ new Ephemeral(), diff --git a/tests/PHPUnit/Unit/Generator/CacheKeyGeneratorTest.php b/tests/PHPUnit/Unit/Generator/CacheKeyGeneratorTest.php index 244afb3..699ea71 100644 --- a/tests/PHPUnit/Unit/Generator/CacheKeyGeneratorTest.php +++ b/tests/PHPUnit/Unit/Generator/CacheKeyGeneratorTest.php @@ -1,4 +1,6 @@ - { + test(testName, async({page}) => { + await page.goto(PAGE_URL); + + console.log('Opened ' + page.url()) + + const locator = await page.getByText(testName) + + await expect(locator).toHaveClass('pass') + }); + +}) diff --git a/tests/Playwright/yarn.lock b/tests/Playwright/yarn.lock new file mode 100644 index 0000000..a66f5e3 --- /dev/null +++ b/tests/Playwright/yarn.lock @@ -0,0 +1,33 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@playwright/test@^1.34.2": + version "1.35.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.35.0.tgz#532603399a0dd46731fbc31a0df5ce357dafa486" + integrity sha512-6qXdd5edCBynOwsz1YcNfgX8tNWeuS9fxy5o59D0rvHXxRtjXRebB4gE4vFVfEMXl/z8zTnAzfOs7aQDEs8G4Q== + dependencies: + "@types/node" "*" + playwright-core "1.35.0" + optionalDependencies: + fsevents "2.3.2" + +"@types/node@*": + version "20.3.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.0.tgz#719498898d5defab83c3560f45d8498f58d11938" + integrity sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ== + +dotenv@^16.0.3: + version "16.1.4" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.1.4.tgz#67ac1a10cd9c25f5ba604e4e08bc77c0ebe0ca8c" + integrity sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw== + +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +playwright-core@1.35.0: + version "1.35.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.35.0.tgz#b7871b742b4a5c8714b7fa2f570c280a061cb414" + integrity sha512-muMXyPmIx/2DPrCHOD1H1ePT01o7OdKxKj2ebmCAYvqhUy+Y1bpal7B0rdoxros7YrXI294JT/DWw2LqyiqTPA== diff --git a/wp-stash-test-plugin/src/Test.php b/wp-stash-test-plugin/src/Test.php new file mode 100644 index 0000000..384849a --- /dev/null +++ b/wp-stash-test-plugin/src/Test.php @@ -0,0 +1,68 @@ +test = $test; + $this->assertion = $assertion; + $this->summary = $summary; + } + + public function execute() + { + $result = false; + try { + ob_start(); + ($this->assertion)(); + $result = true; + $message = ob_get_clean(); + if (empty($message)) { + $message = 'No details available'; + } + } catch (\Throwable $exception) { + ob_get_clean(); + $message = $exception->getMessage(); + } + ?> + +
+ + test) + ?> + + + +
+ + + + + + + + +

WP Stash

+
+

Environment

+ execute(); + (new \Inpsyde\WpStashTest\Test( + 'Using WP-Stash', + function () { + $stash = WpStash::instance(); + if (!get_class($stash->driver()) === Composite::class) { + throw new Exception("WP Stash does not use the expected driver"); + } + } + ))->execute(); + ?> +
+
+

Single cache entries

+ execute(); + + (new \Inpsyde\WpStashTest\Test( + 'Set single', + function () { + $key = 'wp-stash.single'; + $expectedValue = uniqid(); + wp_cache_set($key, $expectedValue); + $value = wp_cache_get($key); + if (!$expectedValue === $value) { + throw new Exception("Cache does not return the same value"); + } + } + ))->execute(); + + (new \Inpsyde\WpStashTest\Test( + 'Delete single', + function () { + $key = 'wp-stash.single'; + wp_cache_delete($key); + $value = wp_cache_get($key); + if (!empty($value)) { + throw new Exception("Cache not empty after deleting"); + } + } + ))->execute(); + ?> +
+
+

Cache groups

+ execute(); + + (new \Inpsyde\WpStashTest\Test( + 'Set multiple', + function () { + $values = [ + 'foo' => 1, + 'bar' => 2, + 'baz' => 3, + ]; + wp_cache_set_multiple($values); + $results = wp_cache_get_multiple(['foo', 'bar', 'baz']); + echo 'Expecting the cache to contain this exact array:'.PHP_EOL; + var_dump($results); + if (!empty(array_diff_key($values, $results))) { + throw new Exception("There should be different keys"); + } + if (!empty(array_diff($values, $results))) { + throw new Exception("There should be different values"); + } + } + ))->execute(); + + (new \Inpsyde\WpStashTest\Test( + 'Delete multiple', + function () { + $values = [ + 'foo' => 1, + 'bar' => 2, + 'baz' => 3, + ]; + wp_cache_set_multiple($values); + wp_cache_delete_multiple(['foo', 'bar', 'baz']); + $results = wp_cache_get_multiple(['foo', 'bar', 'baz']); + echo 'Expecting all these values to be false:'.PHP_EOL; + var_dump($results); + if (!empty(array_filter($results))) { + throw new Exception("There should be no truthy values here"); + } + } + ))->execute(); + ?> +
+ + + diff --git a/wp-stash-test-plugin/wp-stash-test-plugin.php b/wp-stash-test-plugin/wp-stash-test-plugin.php index 6da91f8..3f616b3 100644 --- a/wp-stash-test-plugin/wp-stash-test-plugin.php +++ b/wp-stash-test-plugin/wp-stash-test-plugin.php @@ -12,3 +12,54 @@ declare(strict_types=1); +namespace Inpsyde\WpStashTest; +/** + * Super tiny autoloading + */ +spl_autoload_register(static function ($class) { + // project-specific namespace prefix + $prefix = __NAMESPACE__ . '\\'; + + // does the class use the namespace prefix? + $len = strlen($prefix); + if (strncmp($prefix, $class, strlen($prefix)) !== 0) { + // no, move to the next registered autoloader + return; + } + + // get the relative class name + $relativeClass = substr($class, $len); + + // replace the namespace prefix with the base directory, replace namespace + // separators with directory separators in the relative class name, append + // with .php + $file = __DIR__ . '/src/' . str_replace('\\', '/', $relativeClass) . '.php'; + + // if the file exists, require it + file_exists($file) and require $file; +}); +add_action('template_redirect', function () { + $foo=1; + + /** + * Super tiny templating just in case.. + */ + \Closure::fromCallable(function () { + require __DIR__ . '/template.php'; + })->call( + new class ([ + 'foo' => 'bar', + ]) { + public function __construct($data) + { + $this->data = $data; + } + + public function __get($key) + { + return $this->data[$key]; + } + } + ); + exit; +});