From 0590f65aebd4850985f31163ffe2c73ff75c60f8 Mon Sep 17 00:00:00 2001 From: Amaury Balmer Date: Tue, 3 Mar 2026 09:44:58 +0100 Subject: [PATCH 1/6] Add configuration files for coding standards and development tools - Introduced .editorconfig to unify coding style across editors. - Added .gitignore to exclude the vendor directory. - Created .phpcs.xml.dist for custom PHP CodeSniffer ruleset. - Updated composer.json to include development dependencies for PHP CodeSniffer and PHPStan. - Added phpstan configuration files for static analysis. - Enhanced readme.txt with development instructions for PHP quality tools. --- .editorconfig | 23 + .gitignore | 1 + .phpcs.xml.dist | 22 + composer.json | 47 +- composer.lock | 968 ++++++++++++++++++++++++++++++++++++++++++ phpstan-baseline.neon | 31 ++ phpstan.neon | 13 + readme.txt | 14 + 8 files changed, 1106 insertions(+), 13 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .phpcs.xml.dist create mode 100644 composer.lock create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6f09183 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# Unify coding style across editors and IDEs. +# Based on WordPress core .editorconfig. +# See https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab + +[*.{yml,yaml,neon}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.{json,lock}] +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..a1cf180 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,22 @@ + + + Custom ruleset for Batcache (WordPress Coding Standards). + + + + . + + + */vendor/* + */node_modules/* + + + + + + + + + + + diff --git a/composer.json b/composer.json index 7f75899..f7cd00f 100644 --- a/composer.json +++ b/composer.json @@ -1,15 +1,36 @@ { - "name" : "automattic/batcache", - "description": "A memcached HTML page cache for WordPress.", - "homepage" : "https://github.com/Automattic/batcache", - "type" : "wordpress-muplugin", - "license" : "GPL-2.0+", - "support" : { - "issues": "https://github.com/Automattic/batcache/issues", - "forum": "https://wordpress.org/support/plugin/batcache", - "source": "https://github.com/Automattic/batcache" - }, - "require" : { - "composer/installers": "~1.0" - } + "name": "automattic/batcache", + "description": "A memcached HTML page cache for WordPress.", + "homepage": "https://github.com/Automattic/batcache", + "type": "wordpress-muplugin", + "license": "GPL-2.0+", + "support": { + "issues": "https://github.com/Automattic/batcache/issues", + "forum": "https://wordpress.org/support/plugin/batcache", + "source": "https://github.com/Automattic/batcache" + }, + "require": { + "composer/installers": "~1.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "phpcompatibility/phpcompatibility-wp": "^2", + "phpstan/phpstan": "^2.0", + "szepeviktor/phpstan-wordpress": "^2.0", + "wp-coding-standards/wpcs": "^3" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "composer/installers": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "phpstan/extension-installer": false + } + }, + "scripts": { + "lint": "./vendor/bin/phpcs --standard=.phpcs.xml.dist", + "format": "./vendor/bin/phpcbf --standard=.phpcs.xml.dist", + "phpcs": "./vendor/bin/phpcs", + "analyze": "./vendor/bin/phpstan analyse --memory-limit=512M" + } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..8bd4f36 --- /dev/null +++ b/composer.lock @@ -0,0 +1,968 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "505543f6baeb64aeb5fefe04824ce39e", + "packages": [ + { + "name": "composer/installers", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/composer/installers.git", + "reference": "d20a64ed3c94748397ff5973488761b22f6d3f19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/installers/zipball/d20a64ed3c94748397ff5973488761b22f6d3f19", + "reference": "d20a64ed3c94748397ff5973488761b22f6d3f19", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, + "replace": { + "roundcube/plugin-installer": "*", + "shama/baton": "*" + }, + "require-dev": { + "composer/composer": "1.6.* || ^2.0", + "composer/semver": "^1 || ^3", + "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan-phpunit": "^0.12.16", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.3" + }, + "type": "composer-plugin", + "extra": { + "class": "Composer\\Installers\\Plugin", + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Robinson Young", + "email": "kyle@dontkry.com", + "homepage": "https://github.com/shama" + } + ], + "description": "A multi-framework Composer library installer", + "homepage": "https://composer.github.io/installers/", + "keywords": [ + "Craft", + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "MantisBT", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Starbug", + "Thelia", + "Whmcs", + "WolfCMS", + "agl", + "aimeos", + "annotatecms", + "attogram", + "bitrix", + "cakephp", + "chef", + "cockpit", + "codeigniter", + "concrete5", + "croogo", + "dokuwiki", + "drupal", + "eZ Platform", + "elgg", + "expressionengine", + "fuelphp", + "grav", + "installer", + "itop", + "joomla", + "known", + "kohana", + "laravel", + "lavalite", + "lithium", + "magento", + "majima", + "mako", + "mediawiki", + "miaoxing", + "modulework", + "modx", + "moodle", + "osclass", + "pantheon", + "phpbb", + "piwik", + "ppi", + "processwire", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", + "silverstripe", + "sydes", + "sylius", + "symfony", + "tastyigniter", + "typo3", + "wordpress", + "yawik", + "zend", + "zikula" + ], + "support": { + "issues": "https://github.com/composer/installers/issues", + "source": "https://github.com/composer/installers/tree/v1.12.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-09-13T08:19:44+00:00" + } + ], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-11T04:32:07+00:00" + }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.9.1", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^6.0", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "symfony/polyfill-php80": "*", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.1" + }, + "time": "2026-02-03T19:29:21+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.4", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-09-19T17:43:28+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "2.1.8", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0", + "phpcompatibility/phpcompatibility-paragonie": "^1.0", + "squizlabs/php_codesniffer": "^3.3" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-10-18T00:05:59+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "b598aa890815b8df16363271b659d73280129101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-12T23:06:57+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-12-08T14:27:58+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.40", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-02-23T15:04:35+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-04T16:30:35+00:00" + }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/aa722f037b2d034828cd6c55ebe9e5c74961927e", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-stubs/wordpress-stubs": "^6.6.2", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "composer/semver": "^3.4", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "SzepeViktor\\PHPStan\\WordPress\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress extensions for PHPStan", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.3" + }, + "time": "2025-09-14T02:58:22+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2025-11-25T12:08:04+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..536a654 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,31 @@ +parameters: + ignoreErrors: + - + message: '#^Binary operation "\+" between string and string results in an error\.$#' + identifier: binaryOp.invalid + count: 1 + path: advanced-cache.php + + - + message: '#^Call to function batcache_stats\(\) on a separate line has no effect\.$#' + identifier: function.resultUnused + count: 5 + path: advanced-cache.php + + - + message: '#^Call to function is_array\(\) with array\ will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: advanced-cache.php + + - + message: '#^Undefined variable\: \$fun$#' + identifier: variable.undefined + count: 1 + path: advanced-cache.php + + - + message: '#^Variable \$wp_object_cache might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: batcache.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..21be0fd --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,13 @@ +includes: + - phpstan-baseline.neon + - vendor/szepeviktor/phpstan-wordpress/extension.neon + +parameters: + level: 5 + paths: + - advanced-cache.php + - batcache.php + - batcache-stats-example.php + excludePaths: + - vendor/* + - batcache-stats.php (?) diff --git a/readme.txt b/readme.txt index f0bcc76..e8f6dec 100644 --- a/readme.txt +++ b/readme.txt @@ -60,6 +60,20 @@ Actually all of WordPress.com stays up during Apple events because of Batcache. Batcache was named "supercache" when it was written. (It's still called that on WordPress.com.) A few months later, while "supercache" was still private, Donncha released the WP-Super-Cache plugin. It wouldn't be fun to dispute the name or create confusion for users so a name change seemed best. The move from "Super" to "Bat" was inspired by comic book heroes. It has nothing to do with the fact that the author's city is home to the [world's largest urban bat colony](http://www.batcon.org/our-work/regions/usa-canada/protect-mega-populations/cab-intro). +== Development == + += PHP quality tools = + +Install dev dependencies with Composer: + +`composer install` + +* **Lint (PHPCS):** `composer lint` — WordPress Coding Standards (WPCS) +* **Auto-fix (PHPCBF):** `composer format` — fix many lint violations automatically +* **Static analysis (PHPStan):** `composer analyze` — level 5 with WordPress stubs + +Configuration: `.phpcs.xml.dist`, `phpstan.neon`, `phpstan-baseline.neon`, `.editorconfig`. + == Changelog == = 1.5 = From 9063fc413f36923f30c0a57075da505c76465f91 Mon Sep 17 00:00:00 2001 From: Amaury Balmer Date: Tue, 3 Mar 2026 09:45:42 +0100 Subject: [PATCH 2/6] Refactor Batcache code for improved readability and PHP compatibility - Updated function definitions and conditionals for better clarity and consistency. - Enhanced comments for better understanding of functionality. - Adjusted formatting and spacing for improved code style. - Ensured compatibility with PHP 8.2 by declaring used properties and using modern syntax. --- advanced-cache.php | 416 +++++++++++++++++++++---------------- batcache-stats-example.php | 45 +++- batcache.php | 76 ++++--- 3 files changed, 313 insertions(+), 224 deletions(-) diff --git a/advanced-cache.php b/advanced-cache.php index 33f4ea1..5d9576a 100644 --- a/advanced-cache.php +++ b/advanced-cache.php @@ -1,9 +1,10 @@ cancel = true; + } } // Variants can be set by functions which use early-set globals like $_SERVER to run simple tests. // Functions defined in WordPress, plugins, and themes are not available and MUST NOT be used. // Example: vary_cache_on_function('return preg_match("/feedburner/i", $_SERVER["HTTP_USER_AGENT"]);'); -// This will cause batcache to cache a variant for requests from Feedburner. +// This will cause batcache to cache a variant for requests from Feedburner. // Tips for writing $function: -// X_X DO NOT use any functions from your theme or plugins. Those files have not been included. Fatal error. -// X_X DO NOT use any WordPress functions except is_admin() and is_multisite(). Fatal error. -// X_X DO NOT include or require files from anywhere without consulting expensive professionals first. Fatal error. -// X_X DO NOT use $wpdb, $blog_id, $current_user, etc. These have not been initialized. -// ^_^ DO understand how anonymous functions and eval work. This is how your code is used: eval( '$fun = function() { ' . $function . '; };' ); -// ^_^ DO remember to return something. The return value determines the cache variant. -function vary_cache_on_function($function) { +// X_X DO NOT use any functions from your theme or plugins. Those files have not been included. Fatal error. +// X_X DO NOT use any WordPress functions except is_admin() and is_multisite(). Fatal error. +// X_X DO NOT include or require files from anywhere without consulting expensive professionals first. Fatal error. +// X_X DO NOT use $wpdb, $blog_id, $current_user, etc. These have not been initialized. +// ^_^ DO understand how anonymous functions and eval work. This is how your code is used: eval( '$fun = function() { ' . $function . '; };' ); +// ^_^ DO remember to return something. The return value determines the cache variant. +function vary_cache_on_function( $function ) { global $batcache; - if ( preg_match('/include|require|echo|(?add_variant($function); + $batcache->add_variant( $function ); } class batcache { // This is the base configuration. You can edit these variables or move them into your wp-config.php file. - var $max_age = 300; // Expire batcache items aged this many seconds (zero to disable batcache) + var $max_age = 300; // Expire batcache items aged this many seconds (zero to disable batcache) - var $remote = 0; // Zero disables sending buffers to remote datacenters (req/sec is never sent) + var $remote = 0; // Zero disables sending buffers to remote datacenters (req/sec is never sent) - var $times = 2; // Only batcache a page after it is accessed this many times... (two or more) - var $seconds = 120; // ...in this many seconds (zero to ignore this and use batcache immediately) + var $times = 2; // Only batcache a page after it is accessed this many times... (two or more) + var $seconds = 120; // ...in this many seconds (zero to ignore this and use batcache immediately) - var $group = 'batcache'; // Name of memcached group. You can simulate a cache flush by changing this. + var $group = 'batcache'; // Name of memcached group. You can simulate a cache flush by changing this. - var $unique = array(); // If you conditionally serve different content, put the variable values here. + var $unique = array(); // If you conditionally serve different content, put the variable values here. - var $vary = array(); // Array of functions for anonymous function eval. The return value is added to $unique above. + var $vary = array(); // Array of functions for anonymous function eval. The return value is added to $unique above. var $headers = array(); // Add headers here as name=>value or name=>array(values). These will be sent with every response from the cache. - var $status_header = false; - var $cache_redirects = false; // Set true to enable redirect caching. - var $redirect_status = false; // This is set to the response code during a redirect. + var $status_header = false; + var $cache_redirects = false; // Set true to enable redirect caching. + var $redirect_status = false; // This is set to the response code during a redirect. var $redirect_location = false; // This is set to the redirect location. var $use_stale = true; // Is it ok to return stale cached response when updating the cache? - var $uncached_headers = array('transfer-encoding'); // These headers will never be cached. Apply strtolower. + var $uncached_headers = array( 'transfer-encoding' ); // These headers will never be cached. Apply strtolower. - var $debug = true; // Set false to hide the batcache info + var $debug = true; // Set false to hide the batcache info var $cache_control = true; // Set false to disable Last-Modified and Cache-Control headers var $cancel = false; // Change this to cancel the output buffer. Use batcache_cancel(); - var $cookie = ''; - var $noskip_cookies = array( 'wordpress_test_cookie' ); // Names of cookies - if they exist and the cache would normally be bypassed, don't bypass it + var $cookie = ''; + var $noskip_cookies = array( 'wordpress_test_cookie' ); // Names of cookies - if they exist and the cache would normally be bypassed, don't bypass it var $cacheable_origin_hostnames = array(); // A whitelist of HTTP origin `:` (or just ``) names that are allowed as cache variations. - var $origin = null; // Current Origin header. - var $query = array(); + var $origin = null; // Current Origin header. + var $query = array(); var $ignored_query_args = array(); - var $genlock = false; - var $do = false; + var $genlock = false; + var $do = false; - //Declare used variables for PHP 8.2+ + // Declare used variables for PHP 8.2+ var $cache = array(); var $key = ''; var $keys = array(); @@ -92,31 +96,38 @@ class batcache { var $url_version = null; function __construct( $settings ) { - if ( is_array( $settings ) ) foreach ( $settings as $k => $v ) - $this->$k = $v; + if ( is_array( $settings ) ) { + foreach ( $settings as $k => $v ) { + $this->$k = $v; + } + } } function is_ssl() { - if ( isset($_SERVER['HTTPS']) ) { - if ( 'on' == strtolower($_SERVER['HTTPS']) ) + if ( isset( $_SERVER['HTTPS'] ) ) { + if ( 'on' == strtolower( $_SERVER['HTTPS'] ) ) { return true; - if ( '1' == $_SERVER['HTTPS'] ) + } + if ( '1' == $_SERVER['HTTPS'] ) { return true; - } elseif ( isset($_SERVER['SERVER_PORT']) && ( '443' == $_SERVER['SERVER_PORT'] ) ) { + } + } elseif ( isset( $_SERVER['SERVER_PORT'] ) && ( '443' == $_SERVER['SERVER_PORT'] ) ) { return true; } return false; } function client_accepts_only_json() { - if ( ! isset( $_SERVER['HTTP_ACCEPT'] ) ) + if ( ! isset( $_SERVER['HTTP_ACCEPT'] ) ) { return false; + } $is_json_only = false; foreach ( explode( ',', $_SERVER['HTTP_ACCEPT'] ) as $mime_type ) { - if ( false !== $pos = strpos( $mime_type, ';' ) ) + if ( false !== $pos = strpos( $mime_type, ';' ) ) { $mime_type = substr( $mime_type, 0, $pos ); + } $mime_type = trim( $mime_type ); @@ -138,9 +149,9 @@ function is_cacheable_origin( $origin ) { return false; } - $origin_host = ! empty( $parsed_origin['host'] ) ? strtolower( $parsed_origin['host'] ) : null; + $origin_host = ! empty( $parsed_origin['host'] ) ? strtolower( $parsed_origin['host'] ) : null; $origin_scheme = ! empty( $parsed_origin['scheme'] ) ? strtolower( $parsed_origin['scheme'] ) : null; - $origin_port = ! empty( $parsed_origin['port'] ) ? $parsed_origin['port'] : null; + $origin_port = ! empty( $parsed_origin['port'] ) ? $parsed_origin['port'] : null; return $origin && $origin_host @@ -151,14 +162,14 @@ function is_cacheable_origin( $origin ) { function status_header( $status_header, $status_code ) { $this->status_header = $status_header; - $this->status_code = $status_code; + $this->status_code = $status_code; return $status_header; } function redirect_status( $status, $location ) { if ( $this->cache_redirects ) { - $this->redirect_status = $status; + $this->redirect_status = $status; $this->redirect_location = $location; } @@ -168,16 +179,17 @@ function redirect_status( $status, $location ) { function do_headers( $headers1, $headers2 = array() ) { // Merge the arrays of headers into one $headers = array(); - $keys = array_unique( array_merge( array_keys( $headers1 ), array_keys( $headers2 ) ) ); + $keys = array_unique( array_merge( array_keys( $headers1 ), array_keys( $headers2 ) ) ); foreach ( $keys as $k ) { - $headers[$k] = array(); - if ( isset( $headers1[$k] ) && isset( $headers2[$k] ) ) - $headers[$k] = array_merge( (array) $headers2[$k], (array) $headers1[$k] ); - elseif ( isset( $headers2[$k] ) ) - $headers[$k] = (array) $headers2[$k]; - else - $headers[$k] = (array) $headers1[$k]; - $headers[$k] = array_unique( $headers[$k] ); + $headers[ $k ] = array(); + if ( isset( $headers1[ $k ] ) && isset( $headers2[ $k ] ) ) { + $headers[ $k ] = array_merge( (array) $headers2[ $k ], (array) $headers1[ $k ] ); + } elseif ( isset( $headers2[ $k ] ) ) { + $headers[ $k ] = (array) $headers2[ $k ]; + } else { + $headers[ $k ] = (array) $headers1[ $k ]; + } + $headers[ $k ] = array_unique( $headers[ $k ] ); } // These headers take precedence over any previously sent with the same names foreach ( $headers as $k => $values ) { @@ -191,28 +203,32 @@ function do_headers( $headers1, $headers2 = array() ) { function configure_groups() { // Configure the memcached client - if ( ! $this->remote ) - if ( function_exists('wp_cache_add_no_remote_groups') ) - wp_cache_add_no_remote_groups(array($this->group)); - if ( function_exists('wp_cache_add_global_groups') ) - wp_cache_add_global_groups(array($this->group)); + if ( ! $this->remote ) { + if ( function_exists( 'wp_cache_add_no_remote_groups' ) ) { + wp_cache_add_no_remote_groups( array( $this->group ) ); + } + } + if ( function_exists( 'wp_cache_add_global_groups' ) ) { + wp_cache_add_global_groups( array( $this->group ) ); + } } // Defined here because timer_stop() calls number_format_i18n() - function timer_stop($display = 0, $precision = 3) { + function timer_stop( $display = 0, $precision = 3 ) { global $timestart, $timeend; - $mtime = microtime(); - $mtime = explode(' ',$mtime); - $mtime = $mtime[1] + $mtime[0]; - $timeend = $mtime; - $timetotal = $timeend-$timestart; - $r = number_format($timetotal, $precision); - if ( $display ) + $mtime = microtime(); + $mtime = explode( ' ', $mtime ); + $mtime = $mtime[1] + $mtime[0]; + $timeend = $mtime; + $timetotal = $timeend - $timestart; + $r = number_format( $timetotal, $precision ); + if ( $display ) { echo $r; + } return $r; } - function ob($output) { + function ob( $output ) { // PHP5 and objects disappearing before output buffers? wp_cache_init(); @@ -225,41 +241,42 @@ function ob($output) { } // Do not batcache blank pages unless they are HTTP redirects - $output = trim($output); - if ( $output === '' && (!$this->redirect_status || !$this->redirect_location) ) { + $output = trim( $output ); + if ( $output === '' && ( ! $this->redirect_status || ! $this->redirect_location ) ) { wp_cache_delete( "{$this->url_key}_genlock", $this->group ); return; } // Do not cache 5xx responses - if ( isset( $this->status_code ) && intval($this->status_code / 100) == 5 ) { + if ( isset( $this->status_code ) && intval( $this->status_code / 100 ) == 5 ) { wp_cache_delete( "{$this->url_key}_genlock", $this->group ); return $output; } - $this->do_variants($this->vary); + $this->do_variants( $this->vary ); $this->generate_keys(); // Construct and save the batcache $this->cache = array( - 'output' => $output, - 'time' => isset( $_SERVER['REQUEST_TIME'] ) ? $_SERVER['REQUEST_TIME'] : time(), - 'timer' => $this->timer_stop(false, 3), - 'headers' => array(), - 'status_header' => $this->status_header, - 'redirect_status' => $this->redirect_status, + 'output' => $output, + 'time' => isset( $_SERVER['REQUEST_TIME'] ) ? $_SERVER['REQUEST_TIME'] : time(), + 'timer' => $this->timer_stop( false, 3 ), + 'headers' => array(), + 'status_header' => $this->status_header, + 'redirect_status' => $this->redirect_status, 'redirect_location' => $this->redirect_location, - 'version' => $this->url_version + 'version' => $this->url_version, ); foreach ( headers_list() as $header ) { - list($k, $v) = array_map('trim', explode(':', $header, 2)); - $this->cache['headers'][$k][] = $v; + list($k, $v) = array_map( 'trim', explode( ':', $header, 2 ) ); + $this->cache['headers'][ $k ][] = $v; } - if ( !empty( $this->cache['headers'] ) && !empty( $this->uncached_headers ) ) { - foreach ( $this->uncached_headers as $header ) - unset( $this->cache['headers'][$header] ); + if ( ! empty( $this->cache['headers'] ) && ! empty( $this->uncached_headers ) ) { + foreach ( $this->uncached_headers as $header ) { + unset( $this->cache['headers'][ $header ] ); + } } foreach ( $this->cache['headers'] as $header => $values ) { @@ -269,24 +286,28 @@ function ob($output) { return $output; } - foreach ( (array) $values as $value ) - if ( preg_match('/^Cache-Control:.*max-?age=(\d+)/i', "$header: $value", $matches) ) - $this->max_age = intval($matches[1]); + foreach ( (array) $values as $value ) { + if ( preg_match( '/^Cache-Control:.*max-?age=(\d+)/i', "$header: $value", $matches ) ) { + $this->max_age = intval( $matches[1] ); + } + } } $this->cache['max_age'] = $this->max_age; - wp_cache_set($this->key, $this->cache, $this->group, $this->max_age + $this->seconds + 30); + wp_cache_set( $this->key, $this->cache, $this->group, $this->max_age + $this->seconds + 30 ); // Unlock regeneration - wp_cache_delete("{$this->url_key}_genlock", $this->group); + wp_cache_delete( "{$this->url_key}_genlock", $this->group ); if ( $this->cache_control ) { // Don't clobber Last-Modified header if already set, e.g. by WP::send_headers() - if ( !isset($this->cache['headers']['Last-Modified']) ) + if ( ! isset( $this->cache['headers']['Last-Modified'] ) ) { header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $this->cache['time'] ) . ' GMT', true ); - if ( !isset($this->cache['headers']['Cache-Control']) ) - header("Cache-Control: max-age=$this->max_age, must-revalidate", false); + } + if ( ! isset( $this->cache['headers']['Cache-Control'] ) ) { + header( "Cache-Control: max-age=$this->max_age, must-revalidate", false ); + } } $this->do_headers( $this->headers ); @@ -301,38 +322,39 @@ function ob($output) { return $this->cache['output']; } - function add_variant($function) { - $key = md5($function); - $this->vary[$key] = $function; + function add_variant( $function ) { + $key = md5( $function ); + $this->vary[ $key ] = $function; } - function do_variants($dimensions = false) { + function do_variants( $dimensions = false ) { // This function is called without arguments early in the page load, then with arguments during the OB handler. - if ( $dimensions === false ) - $dimensions = wp_cache_get("{$this->url_key}_vary", $this->group); - else - wp_cache_set("{$this->url_key}_vary", $dimensions, $this->group, $this->max_age + 10); + if ( $dimensions === false ) { + $dimensions = wp_cache_get( "{$this->url_key}_vary", $this->group ); + } else { + wp_cache_set( "{$this->url_key}_vary", $dimensions, $this->group, $this->max_age + 10 ); + } - if ( is_array($dimensions) ) { - ksort($dimensions); + if ( is_array( $dimensions ) ) { + ksort( $dimensions ); foreach ( $dimensions as $key => $function ) { eval( '$fun = function() { ' . $function . '; };' ); - $value = call_user_func( $fun ); - $this->keys[$key] = $value; + $value = call_user_func( $fun ); + $this->keys[ $key ] = $value; } } } function generate_keys() { // ksort($this->keys); // uncomment this when traffic is slow - $this->key = md5(serialize($this->keys)); + $this->key = md5( serialize( $this->keys ) ); $this->req_key = $this->key . '_reqs'; } function add_debug_just_cached() { $generation = $this->cache['timer']; - $bytes = strlen( serialize( $this->cache ) ); - $html = <<cache ) ); + $html = <<max_age} seconds @@ -344,10 +366,10 @@ function add_debug_just_cached() { function add_debug_from_cache() { $seconds_ago = time() - $this->cache['time']; - $generation = $this->cache['timer']; - $serving = $this->timer_stop( false, 3 ); - $expires = $this->cache['max_age'] - time() + $this->cache['time']; - $html = <<cache['timer']; + $serving = $this->timer_stop( false, 3 ); + $expires = $this->cache['max_age'] - time() + $this->cache['time']; + $html = <<cache['headers'][ $key ][0] ) && 0 !== strpos( $this->cache['headers'][ $key ][0], 'text/html' ) ) + if ( isset( $this->cache['headers'][ $key ][0] ) && 0 !== strpos( $this->cache['headers'][ $key ][0], 'text/html' ) ) { return; + } } $head_position = strpos( $this->cache['output'], 'cookie ) { - if ( ! in_array( $batcache->cookie, $batcache->noskip_cookies ) && ( substr( $batcache->cookie, 0, 2 ) == 'wp' || substr( $batcache->cookie, 0, 9 ) == 'wordpress' || substr( $batcache->cookie, 0, 14 ) == 'comment_author' ) ) { + if ( ! in_array( $batcache->cookie, $batcache->noskip_cookies ) && ( substr( $batcache->cookie, 0, 2 ) == 'wp' || substr( $batcache->cookie, 0, 9 ) == 'WordPress' || substr( $batcache->cookie, 0, 14 ) == 'comment_author' ) ) { batcache_stats( 'batcache', 'cookie_skip' ); return; } @@ -438,8 +465,9 @@ function set_query( $query_string ) { $batcache->origin = $_SERVER['HTTP_ORIGIN']; } -if ( ! include_once( WP_CONTENT_DIR . '/object-cache.php' ) ) +if ( ! include_once WP_CONTENT_DIR . '/object-cache.php' ) { return; +} wp_cache_init(); // Note: wp-settings.php calls wp_cache_init() which clobbers the object made here. @@ -449,35 +477,41 @@ function set_query( $query_string ) { // Now that the defaults are set, you might want to use different settings under certain conditions. -/* Example: if your documents have a mobile variant (a different document served by the same URL) you must tell batcache about the variance. Otherwise you might accidentally cache the mobile version and serve it to desktop users, or vice versa. +/* +Example: if your documents have a mobile variant (a different document served by the same URL) you must tell batcache about the variance. Otherwise you might accidentally cache the mobile version and serve it to desktop users, or vice versa. $batcache->unique['mobile'] = is_mobile_user_agent(); */ -/* Example: never batcache for this host +/* +Example: never batcache for this host if ( $_SERVER['HTTP_HOST'] == 'do-not-batcache-me.com' ) return; */ -/* Example: batcache everything on this host regardless of traffic level +/* +Example: batcache everything on this host regardless of traffic level if ( $_SERVER['HTTP_HOST'] == 'always-batcache-me.com' ) return; */ -/* Example: If you sometimes serve variants dynamically (e.g. referrer search term highlighting) you probably don't want to batcache those variants. Remember this code is run very early in wp-settings.php so plugins are not yet loaded. You will get a fatal error if you try to call an undefined function. Either include your plugin now or define a test function in this file. +/* +Example: If you sometimes serve variants dynamically (e.g. referrer search term highlighting) you probably don't want to batcache those variants. Remember this code is run very early in wp-settings.php so plugins are not yet loaded. You will get a fatal error if you try to call an undefined function. Either include your plugin now or define a test function in this file. if ( include_once( 'plugins/searchterm-highlighter.php') && referrer_has_search_terms() ) return; */ // Disabled -if ( $batcache->max_age < 1 ) +if ( $batcache->max_age < 1 ) { return; +} // Make sure we can increment. If not, turn off the traffic sensor. -if ( ! method_exists( $GLOBALS['wp_object_cache'], 'incr' ) ) +if ( ! method_exists( $GLOBALS['wp_object_cache'], 'incr' ) ) { $batcache->times = 0; +} // Necessary to prevent clients using cached version after login cookies set. If this is a problem, comment it out and remove all Last-Modified headers. -header('Vary: Cookie', false); +header( 'Vary: Cookie', false ); // Things that define a unique page. if ( isset( $_SERVER['QUERY_STRING'] ) ) { @@ -485,47 +519,49 @@ function set_query( $query_string ) { } $batcache->keys = array( - 'host' => $_SERVER['HTTP_HOST'], + 'host' => $_SERVER['HTTP_HOST'], 'method' => $_SERVER['REQUEST_METHOD'], - 'path' => ( $batcache->pos = strpos($_SERVER['REQUEST_URI'], '?') ) ? substr($_SERVER['REQUEST_URI'], 0, $batcache->pos) : $_SERVER['REQUEST_URI'], - 'query' => $batcache->query, - 'extra' => $batcache->unique + 'path' => ( $batcache->pos = strpos( $_SERVER['REQUEST_URI'], '?' ) ) ? substr( $_SERVER['REQUEST_URI'], 0, $batcache->pos ) : $_SERVER['REQUEST_URI'], + 'query' => $batcache->query, + 'extra' => $batcache->unique, ); if ( isset( $batcache->origin ) ) { $batcache->keys['origin'] = $batcache->origin; } -if ( $batcache->is_ssl() ) +if ( $batcache->is_ssl() ) { $batcache->keys['ssl'] = true; +} -# Some plugins return html or json based on the Accept value for the same URL. -if ( $batcache->client_accepts_only_json() ) +// Some plugins return html or json based on the Accept value for the same URL. +if ( $batcache->client_accepts_only_json() ) { $batcache->keys['json'] = true; +} // Recreate the permalink from the URL -$batcache->permalink = 'http://' . $batcache->keys['host'] . $batcache->keys['path'] . ( isset($batcache->keys['query']['p']) ? "?p=" . $batcache->keys['query']['p'] : '' ); -$batcache->url_key = md5($batcache->permalink); +$batcache->permalink = 'http://' . $batcache->keys['host'] . $batcache->keys['path'] . ( isset( $batcache->keys['query']['p'] ) ? '?p=' . $batcache->keys['query']['p'] : '' ); +$batcache->url_key = md5( $batcache->permalink ); $batcache->configure_groups(); -$batcache->url_version = (int) wp_cache_get("{$batcache->url_key}_version", $batcache->group); +$batcache->url_version = (int) wp_cache_get( "{$batcache->url_key}_version", $batcache->group ); $batcache->do_variants(); $batcache->generate_keys(); // Get the batcache -$batcache->cache = wp_cache_get($batcache->key, $batcache->group); -$is_cached = is_array( $batcache->cache ) && isset( $batcache->cache['time'] ); -$has_expired = $is_cached && time() > $batcache->cache['time'] + $batcache->cache['max_age']; +$batcache->cache = wp_cache_get( $batcache->key, $batcache->group ); +$is_cached = is_array( $batcache->cache ) && isset( $batcache->cache['time'] ); +$has_expired = $is_cached && time() > $batcache->cache['time'] + $batcache->cache['max_age']; if ( isset( $batcache->cache['version'] ) && $batcache->cache['version'] != $batcache->url_version ) { // Always refresh the cache if a newer version is available. $batcache->do = true; -} else if ( $batcache->seconds < 1 || $batcache->times < 2 ) { +} elseif ( $batcache->seconds < 1 || $batcache->times < 2 ) { // Cache is empty or has expired and we're caching all requests. $batcache->do = ! $is_cached || $has_expired; } else { // No batcache item found, or ready to sample traffic again at the end of the batcache life? if ( ! $is_cached || time() >= $batcache->cache['time'] + $batcache->max_age - $batcache->seconds ) { - wp_cache_add($batcache->req_key, 0, $batcache->group); - $batcache->requests = wp_cache_incr($batcache->req_key, 1, $batcache->group); + wp_cache_add( $batcache->req_key, 0, $batcache->group ); + $batcache->requests = wp_cache_incr( $batcache->req_key, 1, $batcache->group ); if ( $batcache->requests >= $batcache->times && // visited enough times @@ -543,8 +579,9 @@ function set_query( $query_string ) { } // Obtain cache generation lock -if ( $batcache->do ) - $batcache->genlock = wp_cache_add("{$batcache->url_key}_genlock", 1, $batcache->group, 10); +if ( $batcache->do ) { + $batcache->genlock = wp_cache_add( "{$batcache->url_key}_genlock", 1, $batcache->group, 10 ); +} if ( $is_cached && // We have cache @@ -556,17 +593,17 @@ function set_query( $query_string ) { ) { // Issue redirect if cached and enabled if ( $batcache->cache['redirect_status'] && $batcache->cache['redirect_location'] && $batcache->cache_redirects ) { - $status = $batcache->cache['redirect_status']; + $status = $batcache->cache['redirect_status']; $location = $batcache->cache['redirect_location']; // From vars.php - $is_IIS = (strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false || strpos($_SERVER['SERVER_SOFTWARE'], 'ExpressionDevServer') !== false); + $is_IIS = ( strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS' ) !== false || strpos( $_SERVER['SERVER_SOFTWARE'], 'ExpressionDevServer' ) !== false ); $batcache->do_headers( $batcache->headers ); if ( $is_IIS ) { - header("Refresh: 0;url=$location"); + header( "Refresh: 0;url=$location" ); } else { if ( php_sapi_name() != 'cgi-fcgi' ) { - $texts = array( + $texts = array( 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', @@ -576,40 +613,45 @@ function set_query( $query_string ) { 306 => 'Reserved', 307 => 'Temporary Redirect', ); - $protocol = $_SERVER["SERVER_PROTOCOL"]; - if ( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol ) + $protocol = $_SERVER['SERVER_PROTOCOL']; + if ( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol ) { $protocol = 'HTTP/1.0'; - if ( isset($texts[$status]) ) - header("$protocol $status " . $texts[$status]); - else - header("$protocol 302 Found"); + } + if ( isset( $texts[ $status ] ) ) { + header( "$protocol $status " . $texts[ $status ] ); + } else { + header( "$protocol 302 Found" ); + } } - header("Location: $location"); + header( "Location: $location" ); } exit; } // Respect ETags served with feeds. $three04 = false; - if ( isset( $SERVER['HTTP_IF_NONE_MATCH'] ) && isset( $batcache->cache['headers']['ETag'][0] ) && $_SERVER['HTTP_IF_NONE_MATCH'] == $batcache->cache['headers']['ETag'][0] ) + if ( isset( $SERVER['HTTP_IF_NONE_MATCH'] ) && isset( $batcache->cache['headers']['ETag'][0] ) && $_SERVER['HTTP_IF_NONE_MATCH'] == $batcache->cache['headers']['ETag'][0] ) { $three04 = true; + } // Respect If-Modified-Since. - elseif ( $batcache->cache_control && isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ) { - $client_time = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); - if ( isset($batcache->cache['headers']['Last-Modified'][0]) ) - $cache_time = strtotime($batcache->cache['headers']['Last-Modified'][0]); - else + elseif ( $batcache->cache_control && isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { + $client_time = strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ); + if ( isset( $batcache->cache['headers']['Last-Modified'][0] ) ) { + $cache_time = strtotime( $batcache->cache['headers']['Last-Modified'][0] ); + } else { $cache_time = $batcache->cache['time']; + } - if ( $client_time >= $cache_time ) + if ( $client_time >= $cache_time ) { $three04 = true; + } } // Use the batcache save time for Last-Modified so we can issue "304 Not Modified" but don't clobber a cached Last-Modified header. - if ( $batcache->cache_control && !isset($batcache->cache['headers']['Last-Modified'][0]) ) { + if ( $batcache->cache_control && ! isset( $batcache->cache['headers']['Last-Modified'][0] ) ) { header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $batcache->cache['time'] ) . ' GMT', true ); - header('Cache-Control: max-age=' . ($batcache->cache['max_age'] - time() + $batcache->cache['time']) . ', must-revalidate', true); + header( 'Cache-Control: max-age=' . ( $batcache->cache['max_age'] - time() + $batcache->cache['time'] ) . ', must-revalidate', true ); } // Add some debug info just before @@ -620,32 +662,40 @@ function set_query( $query_string ) { $batcache->do_headers( $batcache->headers, $batcache->cache['headers'] ); if ( $three04 ) { - header("HTTP/1.1 304 Not Modified", true, 304); + header( 'HTTP/1.1 304 Not Modified', true, 304 ); die; } - if ( !empty($batcache->cache['status_header']) ) - header($batcache->cache['status_header'], true); + if ( ! empty( $batcache->cache['status_header'] ) ) { + header( $batcache->cache['status_header'], true ); + } batcache_stats( 'batcache', 'total_cached_views' ); // Have you ever heard a death rattle before? - die($batcache->cache['output']); + die( $batcache->cache['output'] ); } // Didn't meet the minimum condition? -if ( ! $batcache->do || ! $batcache->genlock ) +if ( ! $batcache->do || ! $batcache->genlock ) { return; +} -//WordPress 4.7 changes how filters are hooked. Since WordPress 4.6 add_filter can be used in advanced-cache.php. Previous behaviour is kept for backwards compatability with WP < 4.6 +// WordPress 4.7 changes how filters are hooked. Since WordPress 4.6 add_filter can be used in advanced-cache.php. Previous behaviour is kept for backwards compatability with WP < 4.6 if ( function_exists( 'add_filter' ) ) { add_filter( 'status_header', array( &$batcache, 'status_header' ), 10, 2 ); add_filter( 'wp_redirect_status', array( &$batcache, 'redirect_status' ), 10, 2 ); } else { - $wp_filter['status_header'][10]['batcache'] = array( 'function' => array(&$batcache, 'status_header'), 'accepted_args' => 2 ); - $wp_filter['wp_redirect_status'][10]['batcache'] = array( 'function' => array(&$batcache, 'redirect_status'), 'accepted_args' => 2 ); + $wp_filter['status_header'][10]['batcache'] = array( + 'function' => array( &$batcache, 'status_header' ), + 'accepted_args' => 2, + ); + $wp_filter['wp_redirect_status'][10]['batcache'] = array( + 'function' => array( &$batcache, 'redirect_status' ), + 'accepted_args' => 2, + ); } -ob_start(array(&$batcache, 'ob')); +ob_start( array( &$batcache, 'ob' ) ); // It is safer to omit the final PHP closing tag. diff --git a/batcache-stats-example.php b/batcache-stats-example.php index cd39abd..e4c657d 100644 --- a/batcache-stats-example.php +++ b/batcache-stats-example.php @@ -1,32 +1,55 @@ configure_groups(); -// Regen home and permalink on posts and pages -add_action('clean_post_cache', 'batcache_post', 10, 2); +// Regen home and permalink on posts and pages. +add_action( 'clean_post_cache', 'batcache_post', 10, 2 ); -// Regen permalink on comments (TODO) -//add_action('comment_post', 'batcache_comment'); -//add_action('wp_set_comment_status', 'batcache_comment'); -//add_action('edit_comment', 'batcache_comment'); +// Optional: regen permalink on comment post, status change, or edit (uncomment add_action calls as needed). -function batcache_post($post_id, $post = null) { +/** + * Clears batcache for the given URLs when a post is updated. + * + * @param int $post_id Post ID. + * @param WP_Post|null $post Post object. Optional, for backwards compatibility. + */ +function batcache_post( $post_id, $post = null ) { global $batcache; - // Get the post for backwards compatibility with earlier versions of WordPress + // Get the post for backwards compatibility with earlier versions of WordPress. if ( ! $post ) { - $post = get_post( $post_id ); + $post = get_post( $post_id ); } - - if ( ! $post || $post->post_type == 'revision' || ! in_array( get_post_status($post_id), array( 'publish', 'trash' ) ) ) + + if ( ! $post || 'revision' === $post->post_type || ! in_array( get_post_status( $post_id ), array( 'publish', 'trash' ), true ) ) { return; + } - $home = trailingslashit( get_option('home') ); + $home = trailingslashit( get_option( 'home' ) ); batcache_clear_url( $home ); batcache_clear_url( $home . 'feed/' ); - batcache_clear_url( get_permalink($post_id) ); + batcache_clear_url( get_permalink( $post_id ) ); } -function batcache_clear_url($url) { +/** + * Clears batcache for a given URL by incrementing its version key. + * + * @param string $url URL to clear from cache. + * @return int|false Cache version after increment on success, false on failure. + */ +function batcache_clear_url( $url ) { global $batcache, $wp_object_cache; - if ( empty($url) ) + if ( empty( $url ) ) { return false; + } - if ( 0 === strpos( $url, 'https://' ) ) + if ( 0 === strpos( $url, 'https://' ) ) { $url = str_replace( 'https://', 'http://', $url ); - if ( 0 !== strpos( $url, 'http://' ) ) + } + if ( 0 !== strpos( $url, 'http://' ) ) { $url = 'http://' . $url; + } $url_key = md5( $url ); - wp_cache_add("{$url_key}_version", 0, $batcache->group); - $retval = wp_cache_incr("{$url_key}_version", 1, $batcache->group); + wp_cache_add( "{$url_key}_version", 0, $batcache->group ); + $retval = wp_cache_incr( "{$url_key}_version", 1, $batcache->group ); - $batcache_no_remote_group_key = array_search( $batcache->group, (array) $wp_object_cache->no_remote_groups ); + $batcache_no_remote_group_key = array_search( $batcache->group, (array) $wp_object_cache->no_remote_groups, true ); if ( false !== $batcache_no_remote_group_key ) { // The *_version key needs to be replicated remotely, otherwise invalidation won't work. // The race condition here should be acceptable. From e8e263a51e5a04089a9771a842f9351e2a1537fc Mon Sep 17 00:00:00 2001 From: Amaury Balmer Date: Tue, 3 Mar 2026 12:49:36 +0100 Subject: [PATCH 3/6] Enhance Batcache configuration and documentation - Updated .phpcs.xml.dist to include additional rules and configurations for improved code quality checks. - Added detailed comments and documentation in advanced-cache.php for better understanding of functionality and parameters. - Adjusted minimum supported PHP and WordPress versions in the configuration to align with current standards. --- .phpcs.xml.dist | 40 ++- advanced-cache.php | 613 +++++++++++++++++++++++++++++++++------------ 2 files changed, 489 insertions(+), 164 deletions(-) diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index a1cf180..b790e2c 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -2,21 +2,51 @@ Custom ruleset for Batcache (WordPress Coding Standards). - - + + + + + + + + + + + + + + + + . - */vendor/* - */node_modules/* + vendor/* + + + advanced-cache\.php + + + advanced-cache\.php + + + + advanced-cache\.php + + - + + + + diff --git a/advanced-cache.php b/advanced-cache.php index 5d9576a..188e929 100644 --- a/advanced-cache.php +++ b/advanced-cache.php @@ -1,14 +1,35 @@ add_variant( $function ); } +/** + * Batcache handler: output buffering, cache keys, and cache serve logic. + * + * phpcs:disable PEAR.NamingConventions.ValidClassName.StartWithCapital -- Legacy lowercase name for backward compatibility. + */ class batcache { - // This is the base configuration. You can edit these variables or move them into your wp-config.php file. - var $max_age = 300; // Expire batcache items aged this many seconds (zero to disable batcache) - - var $remote = 0; // Zero disables sending buffers to remote datacenters (req/sec is never sent) - - var $times = 2; // Only batcache a page after it is accessed this many times... (two or more) - var $seconds = 120; // ...in this many seconds (zero to ignore this and use batcache immediately) - - var $group = 'batcache'; // Name of memcached group. You can simulate a cache flush by changing this. - - var $unique = array(); // If you conditionally serve different content, put the variable values here. - - var $vary = array(); // Array of functions for anonymous function eval. The return value is added to $unique above. - - var $headers = array(); // Add headers here as name=>value or name=>array(values). These will be sent with every response from the cache. - - var $status_header = false; - var $cache_redirects = false; // Set true to enable redirect caching. - var $redirect_status = false; // This is set to the response code during a redirect. - var $redirect_location = false; // This is set to the redirect location. - - var $use_stale = true; // Is it ok to return stale cached response when updating the cache? - var $uncached_headers = array( 'transfer-encoding' ); // These headers will never be cached. Apply strtolower. - - var $debug = true; // Set false to hide the batcache info - - var $cache_control = true; // Set false to disable Last-Modified and Cache-Control headers - - var $cancel = false; // Change this to cancel the output buffer. Use batcache_cancel(); - var $cookie = ''; - var $noskip_cookies = array( 'wordpress_test_cookie' ); // Names of cookies - if they exist and the cache would normally be bypassed, don't bypass it - var $cacheable_origin_hostnames = array(); // A whitelist of HTTP origin `:` (or just ``) names that are allowed as cache variations. - - var $origin = null; // Current Origin header. - var $query = array(); - var $ignored_query_args = array(); - var $genlock = false; - var $do = false; - - // Declare used variables for PHP 8.2+ - var $cache = array(); - var $key = ''; - var $keys = array(); - var $permalink = ''; - var $pos = 0; - var $req_key = ''; - var $requests = 0; - var $status_code = null; - var $url_key = ''; - var $url_version = null; - - function __construct( $settings ) { + /** + * Expire batcache items aged this many seconds (zero to disable batcache). + * + * @var int + */ + public $max_age = 300; + + /** + * Zero disables sending buffers to remote datacenters (req/sec is never sent). + * + * @var int + */ + public $remote = 0; + + /** + * Only batcache a page after it is accessed this many times (two or more). + * + * @var int + */ + public $times = 2; + /** + * In this many seconds (zero to ignore and use batcache immediately). + * + * @var int + */ + public $seconds = 120; + + /** + * Name of memcached group. Change to simulate a cache flush. + * + * @var string + */ + public $group = 'batcache'; + + /** + * If you conditionally serve different content, put the variable values here. + * + * @var array + */ + public $unique = array(); + + /** + * Variant callback strings. Return value is added to $unique. + * + * @var array + */ + public $vary = array(); + + /** + * Headers as name=>value or name=>array(values). Sent with every cached response. + * + * @var array + */ + public $headers = array(); + + /** + * Status header line. + * + * @var string|false + */ + public $status_header = false; + /** + * Set true to enable redirect caching. + * + * @var bool + */ + public $cache_redirects = false; + /** + * Set to the response code during a redirect. + * + * @var int|false + */ + public $redirect_status = false; + /** + * Set to the redirect location. + * + * @var string|false + */ + public $redirect_location = false; + + /** + * Is it ok to return stale cached response when updating the cache? + * + * @var bool + */ + public $use_stale = true; + /** + * These headers will never be cached. Apply strtolower. + * + * @var array + */ + public $uncached_headers = array( 'transfer-encoding' ); + + /** + * Set false to hide the batcache info HTML comment. + * + * @var bool + */ + public $debug = true; + + /** + * Set false to disable Last-Modified and Cache-Control headers. + * + * @var bool + */ + public $cache_control = true; + + /** + * Set true to cancel the output buffer (e.g. via batcache_cancel()). + * + * @var bool + */ + public $cancel = false; + + /** + * Cookie name to check. + * + * @var string + */ + public $cookie = ''; + /** + * Names of cookies that, if present, do not bypass cache. + * + * @var array + */ + public $noskip_cookies = array( 'wordpress_test_cookie' ); + /** + * Whitelist of HTTP Origin host[:port] names allowed as cache variations. + * + * @var array + */ + public $cacheable_origin_hostnames = array(); + + /** + * Current Origin header. + * + * @var string|null + */ + public $origin = null; + /** + * Query args. + * + * @var array + */ + public $query = array(); + /** + * Query args to ignore when building cache key. + * + * @var array + */ + public $ignored_query_args = array(); + /** + * Genlock. + * + * @var bool|int + */ + public $genlock = false; + /** + * Whether to regenerate cache. + * + * @var bool + */ + public $do = false; + + /** + * Cached entry. + * + * @var array + */ + public $cache = array(); + /** + * Cache key. + * + * @var string + */ + public $key = ''; + /** + * Key components. + * + * @var array + */ + public $keys = array(); + /** + * Permalink. + * + * @var string + */ + public $permalink = ''; + /** + * Position. + * + * @var int + */ + public $pos = 0; + /** + * Request key. + * + * @var string + */ + public $req_key = ''; + /** + * Request count. + * + * @var int + */ + public $requests = 0; + /** + * HTTP status code. + * + * @var int|null + */ + public $status_code = null; + /** + * URL key. + * + * @var string + */ + public $url_key = ''; + /** + * URL version. + * + * @var int|null + */ + public $url_version = null; + + /** + * Constructor. + * + * @param array|null $settings Optional. Key-value overrides for defaults. + */ + public function __construct( $settings ) { if ( is_array( $settings ) ) { foreach ( $settings as $k => $v ) { $this->$k = $v; @@ -103,21 +307,31 @@ function __construct( $settings ) { } } - function is_ssl() { + /** + * Whether the request is over HTTPS. + * + * @return bool + */ + public function is_ssl() { if ( isset( $_SERVER['HTTPS'] ) ) { - if ( 'on' == strtolower( $_SERVER['HTTPS'] ) ) { + if ( 'on' === strtolower( $_SERVER['HTTPS'] ) ) { return true; } - if ( '1' == $_SERVER['HTTPS'] ) { + if ( '1' === $_SERVER['HTTPS'] ) { return true; } - } elseif ( isset( $_SERVER['SERVER_PORT'] ) && ( '443' == $_SERVER['SERVER_PORT'] ) ) { + } elseif ( isset( $_SERVER['SERVER_PORT'] ) && '443' === $_SERVER['SERVER_PORT'] ) { return true; } return false; } - function client_accepts_only_json() { + /** + * Whether the client Accept header requests only JSON. + * + * @return bool + */ + public function client_accepts_only_json() { if ( ! isset( $_SERVER['HTTP_ACCEPT'] ) ) { return false; } @@ -125,7 +339,8 @@ function client_accepts_only_json() { $is_json_only = false; foreach ( explode( ',', $_SERVER['HTTP_ACCEPT'] ) as $mime_type ) { - if ( false !== $pos = strpos( $mime_type, ';' ) ) { + $pos = strpos( $mime_type, ';' ); + if ( false !== $pos ) { $mime_type = substr( $mime_type, 0, $pos ); } @@ -142,8 +357,15 @@ function client_accepts_only_json() { return $is_json_only; } - function is_cacheable_origin( $origin ) { - $parsed_origin = parse_url( $origin ); + /** + * Whether the given Origin header is in the cacheable whitelist. + * + * @param string $origin Origin header value. + * @return bool + */ + public function is_cacheable_origin( $origin ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Loaded before WordPress; wp_parse_url may be unavailable. + $parsed_origin = function_exists( 'wp_parse_url' ) ? wp_parse_url( $origin ) : parse_url( $origin ); if ( false === $parsed_origin ) { return false; @@ -160,14 +382,28 @@ function is_cacheable_origin( $origin ) { && in_array( $origin_host, $this->cacheable_origin_hostnames, true ); } - function status_header( $status_header, $status_code ) { + /** + * Filters status_header to capture status and code. + * + * @param string $status_header Header line. + * @param int $status_code HTTP status code. + * @return string + */ + public function status_header( $status_header, $status_code ) { $this->status_header = $status_header; $this->status_code = $status_code; return $status_header; } - function redirect_status( $status, $location ) { + /** + * Filters wp_redirect_status to capture redirect status and location. + * + * @param int $status Redirect status code. + * @param string $location Redirect location. + * @return int + */ + public function redirect_status( $status, $location ) { if ( $this->cache_redirects ) { $this->redirect_status = $status; $this->redirect_location = $location; @@ -176,8 +412,14 @@ function redirect_status( $status, $location ) { return $status; } - function do_headers( $headers1, $headers2 = array() ) { - // Merge the arrays of headers into one + /** + * Sends merged headers to the client. + * + * @param array $headers1 First set of headers. + * @param array $headers2 Optional. Second set. Default empty. + */ + public function do_headers( $headers1, $headers2 = array() ) { + // Merge the arrays of headers into one. $headers = array(); $keys = array_unique( array_merge( array_keys( $headers1 ), array_keys( $headers2 ) ) ); foreach ( $keys as $k ) { @@ -191,7 +433,7 @@ function do_headers( $headers1, $headers2 = array() ) { } $headers[ $k ] = array_unique( $headers[ $k ] ); } - // These headers take precedence over any previously sent with the same names + // These headers take precedence over any previously sent with the same names. foreach ( $headers as $k => $values ) { $clobber = true; foreach ( $values as $v ) { @@ -201,8 +443,11 @@ function do_headers( $headers1, $headers2 = array() ) { } } - function configure_groups() { - // Configure the memcached client + /** + * Configures memcached groups (no-remote, global). + */ + public function configure_groups() { + // Configure the memcached client. if ( ! $this->remote ) { if ( function_exists( 'wp_cache_add_no_remote_groups' ) ) { wp_cache_add_no_remote_groups( array( $this->group ) ); @@ -213,42 +458,59 @@ function configure_groups() { } } - // Defined here because timer_stop() calls number_format_i18n() - function timer_stop( $display = 0, $precision = 3 ) { + /** + * Returns (and optionally echoes) elapsed time since $timestart. + * + * Defined here because WordPress timer_stop() calls number_format_i18n() which may not be loaded. + * + * @param int $display Optional. 1 to echo. Default 0. + * @param int $precision Optional. Decimal places. Default 3. + * @return string Formatted elapsed time. + */ + public function timer_stop( $display = 0, $precision = 3 ) { global $timestart, $timeend; - $mtime = microtime(); - $mtime = explode( ' ', $mtime ); - $mtime = $mtime[1] + $mtime[0]; + + $mtime = microtime(); + $mtime = explode( ' ', $mtime ); + $mtime = $mtime[1] + $mtime[0]; + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Required for WordPress timer_stop() compatibility. $timeend = $mtime; $timetotal = $timeend - $timestart; $r = number_format( $timetotal, $precision ); if ( $display ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Numeric timer value. echo $r; } return $r; } - function ob( $output ) { + /** + * Output buffer callback: stores page in cache and returns output. + * + * @param string $output Buffered output. + * @return string|null Output or null when skipping. + */ + public function ob( $output ) { // PHP5 and objects disappearing before output buffers? wp_cache_init(); // Remember, $wp_object_cache was clobbered in wp-settings.php so we have to repeat this. $this->configure_groups(); - if ( $this->cancel !== false ) { + if ( false !== $this->cancel ) { wp_cache_delete( "{$this->url_key}_genlock", $this->group ); return $output; } - // Do not batcache blank pages unless they are HTTP redirects + // Do not batcache blank pages unless they are HTTP redirects. $output = trim( $output ); - if ( $output === '' && ( ! $this->redirect_status || ! $this->redirect_location ) ) { + if ( '' === $output && ( ! $this->redirect_status || ! $this->redirect_location ) ) { wp_cache_delete( "{$this->url_key}_genlock", $this->group ); - return; + return null; } - // Do not cache 5xx responses - if ( isset( $this->status_code ) && intval( $this->status_code / 100 ) == 5 ) { + // Do not cache 5xx responses. + if ( isset( $this->status_code ) && 5 === (int) ( $this->status_code / 100 ) ) { wp_cache_delete( "{$this->url_key}_genlock", $this->group ); return $output; } @@ -256,11 +518,11 @@ function ob( $output ) { $this->do_variants( $this->vary ); $this->generate_keys(); - // Construct and save the batcache + // Construct and save the batcache. $this->cache = array( 'output' => $output, 'time' => isset( $_SERVER['REQUEST_TIME'] ) ? $_SERVER['REQUEST_TIME'] : time(), - 'timer' => $this->timer_stop( false, 3 ), + 'timer' => $this->timer_stop( 0, 3 ), 'headers' => array(), 'status_header' => $this->status_header, 'redirect_status' => $this->redirect_status, @@ -280,8 +542,8 @@ function ob( $output ) { } foreach ( $this->cache['headers'] as $header => $values ) { - // Do not cache if cookies were set - if ( strtolower( $header ) === 'set-cookie' ) { + // Do not cache if cookies were set. + if ( 'set-cookie' === strtolower( $header ) ) { wp_cache_delete( "{$this->url_key}_genlock", $this->group ); return $output; } @@ -297,11 +559,11 @@ function ob( $output ) { wp_cache_set( $this->key, $this->cache, $this->group, $this->max_age + $this->seconds + 30 ); - // Unlock regeneration + // Unlock regeneration. wp_cache_delete( "{$this->url_key}_genlock", $this->group ); if ( $this->cache_control ) { - // Don't clobber Last-Modified header if already set, e.g. by WP::send_headers() + // Don't clobber Last-Modified header if already set, e.g. by WP::send_headers(). if ( ! isset( $this->cache['headers']['Last-Modified'] ) ) { header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $this->cache['time'] ) . ' GMT', true ); } @@ -312,24 +574,36 @@ function ob( $output ) { $this->do_headers( $this->headers ); - // Add some debug info just before . if ( $this->debug ) { $this->add_debug_just_cached(); } - // Pass output to next ob handler + // Pass output to next ob handler. batcache_stats( 'batcache', 'total_page_views' ); return $this->cache['output']; } - function add_variant( $function ) { + /** + * Adds a variant callback string. + * + * @param string $function PHP code string for variant (used in eval). + * + * phpcs:ignore Generic.NamingConventions.ReservedKeywordUsedAsFunctionName.Found -- Parameter name matches API. + */ + public function add_variant( $function ) { $key = md5( $function ); $this->vary[ $key ] = $function; } - function do_variants( $dimensions = false ) { - // This function is called without arguments early in the page load, then with arguments during the OB handler. - if ( $dimensions === false ) { + /** + * Runs variant callbacks and populates $this->keys. + * + * @param array|false $dimensions Optional. Variant dimensions from cache or false. + */ + public function do_variants( $dimensions = false ) { + // Called without arguments early in the page load, then with arguments during the OB handler. + if ( false === $dimensions ) { $dimensions = wp_cache_get( "{$this->url_key}_vary", $this->group ); } else { wp_cache_set( "{$this->url_key}_vary", $dimensions, $this->group, $this->max_age + 10 ); @@ -337,7 +611,8 @@ function do_variants( $dimensions = false ) { if ( is_array( $dimensions ) ) { ksort( $dimensions ); - foreach ( $dimensions as $key => $function ) { + foreach ( $dimensions as $key => $function ) { // phpcs:ignore Generic.NamingConventions.ReservedKeywordUsedAsFunctionName.Found -- Variant callback string. + // phpcs:ignore Squiz.PHP.Eval.Discouraged -- Variant callbacks require eval in early load context. eval( '$fun = function() { ' . $function . '; };' ); $value = call_user_func( $fun ); $this->keys[ $key ] = $value; @@ -345,16 +620,24 @@ function do_variants( $dimensions = false ) { } } - function generate_keys() { - // ksort($this->keys); // uncomment this when traffic is slow + /** + * Generates cache key from $this->keys. + */ + public function generate_keys() { + // ksort($this->keys); uncomment when traffic is slow. + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Cache key; no user input. $this->key = md5( serialize( $this->keys ) ); $this->req_key = $this->key . '_reqs'; } - function add_debug_just_cached() { + /** + * Appends "just cached" debug HTML to output. + */ + public function add_debug_just_cached() { $generation = $this->cache['timer']; - $bytes = strlen( serialize( $this->cache ) ); - $html = <<cache ) ); + $html = <<max_age} seconds @@ -364,10 +647,13 @@ function add_debug_just_cached() { $this->add_debug_html_to_output( $html ); } - function add_debug_from_cache() { + /** + * Appends "served from cache" debug HTML to output. + */ + public function add_debug_from_cache() { $seconds_ago = time() - $this->cache['time']; $generation = $this->cache['timer']; - $serving = $this->timer_stop( false, 3 ); + $serving = $this->timer_stop( 0, 3 ); $expires = $this->cache['max_age'] - time() + $this->cache['time']; $html = <<add_debug_html_to_output( $html ); } - function add_debug_html_to_output( $debug_html ) { - // Casing on the Content-Type header is inconsistent + /** + * Injects debug HTML into cached output before . + * + * @param string $debug_html HTML fragment to inject. + */ + public function add_debug_html_to_output( $debug_html ) { + // Casing on the Content-Type header is inconsistent. foreach ( array( 'Content-Type', 'Content-type' ) as $key ) { if ( isset( $this->cache['headers'][ $key ][0] ) && 0 !== strpos( $this->cache['headers'][ $key ][0], 'text/html' ) ) { return; @@ -396,7 +687,12 @@ function add_debug_html_to_output( $debug_html ) { $this->cache['output'] .= "\n$debug_html"; } - function set_query( $query_string ) { + /** + * Parses query string and stores in $this->query (excluding ignored args). + * + * @param string $query_string Query string. + */ + public function set_query( $query_string ) { parse_str( $query_string, $this->query ); foreach ( $this->ignored_query_args as $arg ) { @@ -419,22 +715,19 @@ function set_query( $query_string ) { // Never batcache interactive scripts or API endpoints. if ( in_array( basename( $_SERVER['SCRIPT_FILENAME'] ), - array( - 'wp-app.php', - 'xmlrpc.php', - 'wp-cron.php', - ) + array( 'wp-app.php', 'xmlrpc.php', 'wp-cron.php' ), + true ) ) { return; } -// Never batcache WP javascript generators +// Never batcache WP javascript generators. if ( strstr( $_SERVER['SCRIPT_FILENAME'], 'wp-includes/js' ) ) { return; } // Only cache HEAD and GET requests. -if ( ( isset( $_SERVER['REQUEST_METHOD'] ) && ! in_array( $_SERVER['REQUEST_METHOD'], array( 'GET', 'HEAD' ) ) ) ) { +if ( isset( $_SERVER['REQUEST_METHOD'] ) && ! in_array( $_SERVER['REQUEST_METHOD'], array( 'GET', 'HEAD' ), true ) ) { return; } @@ -447,7 +740,7 @@ function set_query( $query_string ) { // Never batcache when cookies indicate a cache-exempt visitor. if ( is_array( $_COOKIE ) && ! empty( $_COOKIE ) ) { foreach ( array_keys( $_COOKIE ) as $batcache->cookie ) { - if ( ! in_array( $batcache->cookie, $batcache->noskip_cookies ) && ( substr( $batcache->cookie, 0, 2 ) == 'wp' || substr( $batcache->cookie, 0, 9 ) == 'WordPress' || substr( $batcache->cookie, 0, 14 ) == 'comment_author' ) ) { + if ( ! in_array( $batcache->cookie, $batcache->noskip_cookies, true ) && ( 'wp' === substr( $batcache->cookie, 0, 2 ) || 'WordPress' === substr( $batcache->cookie, 0, 9 ) || 'comment_author' === substr( $batcache->cookie, 0, 14 ) ) ) { batcache_stats( 'batcache', 'cookie_skip' ); return; } @@ -500,7 +793,7 @@ function set_query( $query_string ) { return; */ -// Disabled +// Disabled. if ( $batcache->max_age < 1 ) { return; } @@ -518,10 +811,11 @@ function set_query( $query_string ) { $batcache->set_query( $_SERVER['QUERY_STRING'] ); } +$batcache->pos = strpos( $_SERVER['REQUEST_URI'], '?' ); $batcache->keys = array( 'host' => $_SERVER['HTTP_HOST'], 'method' => $_SERVER['REQUEST_METHOD'], - 'path' => ( $batcache->pos = strpos( $_SERVER['REQUEST_URI'], '?' ) ) ? substr( $_SERVER['REQUEST_URI'], 0, $batcache->pos ) : $_SERVER['REQUEST_URI'], + 'path' => ( false !== $batcache->pos ) ? substr( $_SERVER['REQUEST_URI'], 0, $batcache->pos ) : $_SERVER['REQUEST_URI'], 'query' => $batcache->query, 'extra' => $batcache->unique, ); @@ -538,7 +832,7 @@ function set_query( $query_string ) { $batcache->keys['json'] = true; } -// Recreate the permalink from the URL +// Recreate the permalink from the URL. $batcache->permalink = 'http://' . $batcache->keys['host'] . $batcache->keys['path'] . ( isset( $batcache->keys['query']['p'] ) ? '?p=' . $batcache->keys['query']['p'] : '' ); $batcache->url_key = md5( $batcache->permalink ); $batcache->configure_groups(); @@ -546,63 +840,63 @@ function set_query( $query_string ) { $batcache->do_variants(); $batcache->generate_keys(); -// Get the batcache +// Get the batcache. $batcache->cache = wp_cache_get( $batcache->key, $batcache->group ); $is_cached = is_array( $batcache->cache ) && isset( $batcache->cache['time'] ); $has_expired = $is_cached && time() > $batcache->cache['time'] + $batcache->cache['max_age']; -if ( isset( $batcache->cache['version'] ) && $batcache->cache['version'] != $batcache->url_version ) { +if ( isset( $batcache->cache['version'] ) && $batcache->cache['version'] !== $batcache->url_version ) { // Always refresh the cache if a newer version is available. $batcache->do = true; } elseif ( $batcache->seconds < 1 || $batcache->times < 2 ) { // Cache is empty or has expired and we're caching all requests. $batcache->do = ! $is_cached || $has_expired; -} else { +} elseif ( ! $is_cached || time() >= $batcache->cache['time'] + $batcache->max_age - $batcache->seconds ) { // No batcache item found, or ready to sample traffic again at the end of the batcache life? - if ( ! $is_cached || time() >= $batcache->cache['time'] + $batcache->max_age - $batcache->seconds ) { - wp_cache_add( $batcache->req_key, 0, $batcache->group ); - $batcache->requests = wp_cache_incr( $batcache->req_key, 1, $batcache->group ); - - if ( - $batcache->requests >= $batcache->times && // visited enough times - ( - ! $is_cached || // no cache - time() >= $batcache->cache['time'] + $batcache->cache['max_age'] // or cache expired - ) - ) { - wp_cache_delete( $batcache->req_key, $batcache->group ); - $batcache->do = true; - } else { - $batcache->do = false; - } + wp_cache_add( $batcache->req_key, 0, $batcache->group ); + $batcache->requests = wp_cache_incr( $batcache->req_key, 1, $batcache->group ); + + if ( + $batcache->requests >= $batcache->times && // Visited enough times. + ( + ! $is_cached || // No cache. + time() >= $batcache->cache['time'] + $batcache->cache['max_age'] // Or cache expired. + ) + ) { + wp_cache_delete( $batcache->req_key, $batcache->group ); + $batcache->do = true; + } else { + $batcache->do = false; } } -// Obtain cache generation lock +// Obtain cache generation lock. if ( $batcache->do ) { $batcache->genlock = wp_cache_add( "{$batcache->url_key}_genlock", 1, $batcache->group, 10 ); } if ( - $is_cached && // We have cache - ! $batcache->genlock && // We have not obtained cache regeneration lock + $is_cached && // We have cache. + ! $batcache->genlock && // We have not obtained cache regeneration lock. ( - ! $has_expired || // Batcached page that hasn't expired - ( $batcache->do && $batcache->use_stale ) // Regenerating it in another request and can use stale cache + ! $has_expired || // Batcached page that hasn't expired. + ( $batcache->do && $batcache->use_stale ) // Regenerating in another request; can use stale cache. ) ) { - // Issue redirect if cached and enabled + // Issue redirect if cached and enabled. if ( $batcache->cache['redirect_status'] && $batcache->cache['redirect_location'] && $batcache->cache_redirects ) { + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Redirect header vars for this request only. $status = $batcache->cache['redirect_status']; $location = $batcache->cache['redirect_location']; - // From vars.php + // From vars.php. + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- IIS detection for header format. $is_IIS = ( strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS' ) !== false || strpos( $_SERVER['SERVER_SOFTWARE'], 'ExpressionDevServer' ) !== false ); $batcache->do_headers( $batcache->headers ); if ( $is_IIS ) { header( "Refresh: 0;url=$location" ); } else { - if ( php_sapi_name() != 'cgi-fcgi' ) { + if ( 'cgi-fcgi' !== php_sapi_name() ) { $texts = array( 300 => 'Multiple Choices', 301 => 'Moved Permanently', @@ -614,7 +908,7 @@ function set_query( $query_string ) { 307 => 'Temporary Redirect', ); $protocol = $_SERVER['SERVER_PROTOCOL']; - if ( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol ) { + if ( 'HTTP/1.1' !== $protocol && 'HTTP/1.0' !== $protocol ) { $protocol = 'HTTP/1.0'; } if ( isset( $texts[ $status ] ) ) { @@ -630,12 +924,10 @@ function set_query( $query_string ) { // Respect ETags served with feeds. $three04 = false; - if ( isset( $SERVER['HTTP_IF_NONE_MATCH'] ) && isset( $batcache->cache['headers']['ETag'][0] ) && $_SERVER['HTTP_IF_NONE_MATCH'] == $batcache->cache['headers']['ETag'][0] ) { + if ( isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) && isset( $batcache->cache['headers']['ETag'][0] ) && $_SERVER['HTTP_IF_NONE_MATCH'] === $batcache->cache['headers']['ETag'][0] ) { $three04 = true; - } - - // Respect If-Modified-Since. - elseif ( $batcache->cache_control && isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { + } elseif ( $batcache->cache_control && isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { + // Respect If-Modified-Since. $client_time = strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ); if ( isset( $batcache->cache['headers']['Last-Modified'][0] ) ) { $cache_time = strtotime( $batcache->cache['headers']['Last-Modified'][0] ); @@ -648,13 +940,13 @@ function set_query( $query_string ) { } } - // Use the batcache save time for Last-Modified so we can issue "304 Not Modified" but don't clobber a cached Last-Modified header. + // Use the batcache save time for Last-Modified so we can issue "304 Not Modified"; don't clobber a cached Last-Modified header. if ( $batcache->cache_control && ! isset( $batcache->cache['headers']['Last-Modified'][0] ) ) { header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $batcache->cache['time'] ) . ' GMT', true ); header( 'Cache-Control: max-age=' . ( $batcache->cache['max_age'] - time() + $batcache->cache['time'] ) . ', must-revalidate', true ); } - // Add some debug info just before + // Add some debug info just before . if ( $batcache->debug ) { $batcache->add_debug_from_cache(); } @@ -673,6 +965,7 @@ function set_query( $query_string ) { batcache_stats( 'batcache', 'total_cached_views' ); // Have you ever heard a death rattle before? + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Cached HTML output. die( $batcache->cache['output'] ); } @@ -681,15 +974,17 @@ function set_query( $query_string ) { return; } -// WordPress 4.7 changes how filters are hooked. Since WordPress 4.6 add_filter can be used in advanced-cache.php. Previous behaviour is kept for backwards compatability with WP < 4.6 +// WordPress 4.7 changes how filters are hooked. Since 4.6 add_filter can be used here; below is for WP < 4.6. if ( function_exists( 'add_filter' ) ) { add_filter( 'status_header', array( &$batcache, 'status_header' ), 10, 2 ); add_filter( 'wp_redirect_status', array( &$batcache, 'redirect_status' ), 10, 2 ); } else { - $wp_filter['status_header'][10]['batcache'] = array( + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Required for WP < 4.6 filter registration. + $wp_filter['status_header'][10]['batcache'] = array( 'function' => array( &$batcache, 'status_header' ), 'accepted_args' => 2, ); + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Required for WP < 4.6 filter registration. $wp_filter['wp_redirect_status'][10]['batcache'] = array( 'function' => array( &$batcache, 'redirect_status' ), 'accepted_args' => 2, From a9d8cf7adb35362b718baa1e01b60e2486483498 Mon Sep 17 00:00:00 2001 From: Amaury Balmer Date: Tue, 3 Mar 2026 12:52:31 +0100 Subject: [PATCH 4/6] Update Batcache to utilize the WordPress object cache API - Changed references from Memcached to the WordPress object cache API in advanced-cache.php for clarity. - Updated composer.json description to reflect support for various object cache backends. - Revised readme.txt to emphasize compatibility with Redis and other object cache systems, enhancing installation instructions and usage guidelines. --- advanced-cache.php | 8 ++++---- composer.json | 2 +- readme.txt | 18 ++++++++++-------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/advanced-cache.php b/advanced-cache.php index 188e929..6cb1029 100644 --- a/advanced-cache.php +++ b/advanced-cache.php @@ -1,6 +1,6 @@ remote ) { if ( function_exists( 'wp_cache_add_no_remote_groups' ) ) { wp_cache_add_no_remote_groups( array( $this->group ) ); diff --git a/composer.json b/composer.json index f7cd00f..fd9a130 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "automattic/batcache", - "description": "A memcached HTML page cache for WordPress.", + "description": "An object-cache HTML page cache for WordPress (Redis, Memcached, etc.).", "homepage": "https://github.com/Automattic/batcache", "type": "wordpress-muplugin", "license": "GPL-2.0+", diff --git a/readme.txt b/readme.txt index e8f6dec..724ab67 100644 --- a/readme.txt +++ b/readme.txt @@ -1,15 +1,15 @@ === Batcache === Contributors: automattic, andy, orensol, markjaquith, vnsavage, batmoo, yoavf -Tags: cache, memcache, memcached, speed, performance, load, server +Tags: cache, memcache, memcached, redis, object-cache, speed, performance, load, server Requires at least: 3.2 Tested up to: 5.3.2 Stable tag: 1.5 -Batcache uses Memcached to store and serve rendered pages. +Batcache uses the WordPress object cache API to store and serve rendered pages (Redis, Memcached, or any compatible backend). == Description == -Batcache uses Memcached to store and serve rendered pages. It can also optionally cache redirects. It's not as fast as Donncha's WP-Super-Cache but it can be used where file-based caching is not practical or not desired. For instance, any site that is run on more than one server should use Batcache because it allows all servers to use the same storage. +Batcache uses the WordPress object cache API to store and serve rendered pages. It works with any object cache backend compatible with the WordPress Object Cache (e.g. Redis, Memcached). It can also optionally cache redirects. It's not as fast as Donncha's WP-Super-Cache but it can be used where file-based caching is not practical or not desired. For instance, any site that is run on more than one server should use Batcache because it allows all servers to use the same storage. Development testing showed a 40x reduction in page generation times: pages generated in 200ms were served from the cache in 5ms. Traffic simulations with Siege demonstrate that WordPress can handle up to twenty times more traffic with Batcache installed. @@ -24,7 +24,7 @@ Possible future features: == Installation == -1. Get the Memcached backend working. See below. +1. Get a WordPress object cache backend working (Redis, Memcached, or other). See below. 1. Upload `advanced-cache.php` to the `/wp-content/` directory @@ -38,17 +38,19 @@ Possible future features: 1. *Optional* Upload `batcache.php` to the `/wp-content/plugins/` directory. -= Memcached backend = += Object cache backend = -1. Install [memcached](https://memcached.org/) on at least one server. Note the connection info. The default is `127.0.0.1:11211`. +Batcache requires a drop-in or plugin that implements the WordPress Object Cache API (e.g. Redis, Memcached, or another compatible backend). Install and configure one before using Batcache. -1. Install the [PECL memcached extension](http://pecl.php.net/package/memcache) and [Memcached Object Cache](https://wordpress.org/plugins/memcached/). +* **Memcached (reference setup):** Install [memcached](https://memcached.org/) on at least one server (default: `127.0.0.1:11211`), the [PECL memcached extension](http://pecl.php.net/package/memcache), and the [Memcached Object Cache](https://wordpress.org/plugins/memcached/) plugin. This was the original Batcache dependency and remains a common choice. +* **Redis:** Use a Redis object cache drop-in or plugin (e.g. Redis Object Cache) and configure your Redis server. +* Other backends compatible with the WordPress Object Cache API will work as well. == Frequently Asked Questions == = Should I use this? = -Batcache can be used anywhere Memcached is available. WP-Super-Cache is preferred for most blogs. If you have more than one web server, try Batcache. +Batcache can be used with any WordPress object cache backend (e.g. Redis, Memcached). WP-Super-Cache is preferred for most blogs. If you have more than one web server, try Batcache. = Why was this written? = From 0390074de43eb971cd15af846add410ef851bc7a Mon Sep 17 00:00:00 2001 From: Amaury Balmer Date: Tue, 3 Mar 2026 12:54:25 +0100 Subject: [PATCH 5/6] Update readme.txt for version 1.6 - Increment stable tag to 1.6. - Document compatibility with all WordPress object cache API backends, not just Memcached. - Revise code comments and documentation to use "object cache" terminology. - Add a section for Object cache backends in the readme, citing Memcached as a reference. - Introduce PHP quality tooling with Composer dev dependencies and configuration files for code standards. - Ensure code compliance with WordPress Coding Standards through formatting and PHPDoc updates. --- readme.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 724ab67..76c7c5f 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: automattic, andy, orensol, markjaquith, vnsavage, batmoo, yoavf Tags: cache, memcache, memcached, redis, object-cache, speed, performance, load, server Requires at least: 3.2 Tested up to: 5.3.2 -Stable tag: 1.5 +Stable tag: 1.6 Batcache uses the WordPress object cache API to store and serve rendered pages (Redis, Memcached, or any compatible backend). @@ -78,6 +78,14 @@ Configuration: `.phpcs.xml.dist`, `phpstan.neon`, `phpstan-baseline.neon`, `.edi == Changelog == += 1.6 = + +* Document compatibility with any WordPress object cache API backend (Redis, Memcached, etc.), not only Memcached +* Update code comments and docs to use "object cache" instead of "memcached" where generic +* Readme: add Object cache backend section; cite Memcached as reference setup and original dependency +* Add PHP quality tooling: Composer dev dependencies (WPCS, PHPCompatibility-WP, PHPStan with WordPress stubs), `.phpcs.xml.dist`, `phpstan.neon`, `.editorconfig`; `composer lint`, `composer format`, `composer analyze` +* Code changes for PHPCS compliance: WordPress Coding Standards (WordPress-Extra, WordPress-Docs), formatting, PHPDoc, and targeted rule exclusions for drop-in layout + = 1.5 = * Add stats for cache hits From d7b1c84c8d418d32cc3183de80033a88886e14dc Mon Sep 17 00:00:00 2001 From: Amaury Balmer Date: Tue, 3 Mar 2026 22:33:12 +0100 Subject: [PATCH 6/6] Enhance Batcache with DDEV support and admin features - Added DDEV configuration files for local WordPress development. - Updated .gitignore to exclude build and wp directories. - Enhanced .phpcs.xml.dist to exclude additional patterns. - Improved advanced-cache.php and batcache.php with detailed plugin metadata and new admin bar functionality for purging cache. - Documented testing procedures and setup in TESTING.md, including Redis and Memcached integration. - Revised readme.txt to reflect new features and installation instructions. --- .ddev/config.yaml | 8 ++ .ddev/docker-compose.plugin.yaml | 7 ++ .ddev/docker-compose.redis.yaml | 27 ++++++ .gitignore | 2 + .phpcs.xml.dist | 2 + TESTING.md | 122 +++++++++++++++++++++++++++ advanced-cache.php | 15 +++- batcache.php | 137 +++++++++++++++++++++++++++++-- readme.txt | 5 ++ 9 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 .ddev/config.yaml create mode 100644 .ddev/docker-compose.plugin.yaml create mode 100644 .ddev/docker-compose.redis.yaml create mode 100644 TESTING.md diff --git a/.ddev/config.yaml b/.ddev/config.yaml new file mode 100644 index 0000000..921281e --- /dev/null +++ b/.ddev/config.yaml @@ -0,0 +1,8 @@ +# DDEV configuration for Batcache (WordPress plugin). +# One-time setup: create WordPress in wp/ then start (see TESTING.md). +name: batcache +type: wordpress +docroot: wp +php_version: "8.3" +web_environment: + - "WP_ROOT=/var/www/html/wp" diff --git a/.ddev/docker-compose.plugin.yaml b/.ddev/docker-compose.plugin.yaml new file mode 100644 index 0000000..18fddca --- /dev/null +++ b/.ddev/docker-compose.plugin.yaml @@ -0,0 +1,7 @@ +# Mount the plugin (project root) into WordPress plugins directory +# so the repo appears at wp/wp-content/plugins/batcache. +services: + web: + volumes: + - "../.:/var/www/html/wp/wp-content/plugins/batcache" + - "../advanced-cache.php:/var/www/html/wp/wp-content/advanced-cache.php" diff --git a/.ddev/docker-compose.redis.yaml b/.ddev/docker-compose.redis.yaml new file mode 100644 index 0000000..e9da4c6 --- /dev/null +++ b/.ddev/docker-compose.redis.yaml @@ -0,0 +1,27 @@ +#ddev-generated +services: + redis: + container_name: ddev-${DDEV_SITENAME}-redis + image: ${REDIS_DOCKER_IMAGE:-redis:7} + hostname: ${REDIS_HOSTNAME:-redis} + # These labels ensure this service is discoverable by ddev. + labels: + com.ddev.site-name: ${DDEV_SITENAME} + com.ddev.approot: ${DDEV_APPROOT} + restart: "no" + expose: + - 6379 + volumes: + - ".:/mnt/ddev_config" + - "ddev-global-cache:/mnt/ddev-global-cache" + - "./redis:/etc/redis/conf" + - "redis:/data" + command: /etc/redis/conf/redis.conf + x-ddev: + describe-url-port: | + Backend: ${REDIS_DOCKER_IMAGE:-redis:7} + describe-info: | + Pass: + +volumes: + redis: diff --git a/.gitignore b/.gitignore index 57872d0..d8eed33 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /vendor/ +/build/ +/wp/ diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index b790e2c..75ecdff 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -22,6 +22,8 @@ vendor/* + wp/* + .ddev/* diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..273f9e3 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,122 @@ +# Testing Batcache with DDEV + +This project uses [DDEV](https://ddev.com/) for local development and **manual testing** in an isolated WordPress environment. + +## Quick start + +```bash +composer install +mkdir -p wp && ddev start +ddev exec wp core download --path=/var/www/html/wp +ddev exec wp config create --path=/var/www/html/wp --dbname=db --dbuser=db --dbpass=db +ddev exec wp core install --path=/var/www/html/wp --url=https://batcache.ddev.site --title=Batcache --admin_user=admin --admin_password=admin --admin_email=admin@example.com --skip-email +ddev exec wp plugin activate batcache --path=/var/www/html/wp +``` + +## Prerequisites + +- **DDEV** – [ddev.com](https://ddev.com/) (includes Docker) +- **Composer** – [getcomposer.org](https://getcomposer.org/) + +## One-time setup + +1. Install PHP dependencies (optional, for lint/static analysis): + +```bash +composer install +``` + +2. Create the WordPress docroot and start DDEV: + +```bash +mkdir -p wp +ddev start +``` + +3. Install WordPress in `wp/` (first time only): + +```bash +ddev exec wp core download --path=/var/www/html/wp +ddev exec wp config create --path=/var/www/html/wp --dbname=db --dbuser=db --dbpass=db +ddev exec wp core install --path=/var/www/html/wp --url=https://batcache.ddev.site --title=Batcache --admin_user=admin --admin_password=admin --admin_email=admin@example.com --skip-email +ddev exec wp plugin activate batcache --path=/var/www/html/wp +# Optional: install Redis Object Cache for Batcache backend (requires Redis, see below) +ddev exec wp plugin install redis-cache --path=/var/www/html/wp --activate +``` + +The plugin (this repo) is mounted at `wp/wp-content/plugins/batcache` via `.ddev/docker-compose.plugin.yaml`. + +## Manual testing + +There are no automated unit tests. Verify Batcache manually: + +1. **Object cache required.** Ensure an object cache backend is running (e.g. Redis with Redis Object Cache plugin) and that Batcache’s `advanced-cache.php` drop-in is in place and `WP_CACHE` is set. See [Redis or Memcached](#redis-or-memcached) below. + +2. **Cache serving.** Load a public page (e.g. the homepage) several times. View the page source: just above the `` closing tag you should see Batcache stats (e.g. generation time, cache hit). See the main [readme](readme.txt) for details. + +3. **Cache invalidation.** With the optional `batcache.php` plugin active, publish or update a post and confirm the relevant URLs are no longer served from cache (or are regenerated) as expected. + +4. **Redis/Memcached.** If using Redis Object Cache, run `ddev exec wp redis status --path=/var/www/html/wp` to confirm the connection. Exercise the site in the browser and in WP-CLI to ensure no errors. + +## Managing the environment + +```bash +ddev start # Start +ddev stop # Stop +``` + +## Accessing the site + +When DDEV is running, the site is available at **https://batcache.ddev.site** (or the URL shown by `ddev describe`). + +Default admin credentials (after `wp core install` above): `admin` / `admin`. + +## Redis or Memcached + +Batcache needs an object cache backend (e.g. [Redis Object Cache](https://wordpress.org/plugins/redis-cache/)). DDEV supports Redis via an add-on. + +### Redis (recommended) + +1. Add the Redis service and install the plugin: + +```bash +# Add Redis container +ddev get ddev/ddev-redis +ddev restart + +# Install and activate Redis Object Cache plugin (WP-CLI) +ddev exec wp plugin install redis-cache --path=/var/www/html/wp --activate + +# Enable the object cache drop-in +ddev exec wp redis enable --path=/var/www/html/wp +``` + +2. **Configure wp-config for Redis.** WordPress must know the Redis host and have the cache constant set. In DDEV, add these to `wp/wp-config-ddev.php` (or to `wp-config.php` after the block that includes it): + +```php +define( 'WP_REDIS_HOST', 'ddev-batcache-redis' ); +define( 'WP_CACHE', true ); +``` + +The host name is `ddev--redis` (here `batcache`). If you edit `wp-config-ddev.php`, note that DDEV may overwrite it; remove the `#ddev-generated` comment at the top if you want to keep your changes, or put these defines in `wp-config.php` instead. + +3. Check status: `ddev exec wp redis status --path=/var/www/html/wp` + +To disable the object cache (e.g. for imports or WP-CLI batch jobs): `ddev exec wp redis disable --path=/var/www/html/wp`. + +### Memcached + +If a Memcached add-on is available, install it and use a Memcached object cache plugin; configure it to use the service (e.g. `memcached:11211`). + +## Configuration files + +- **`.ddev/config.yaml`** – DDEV project config (WordPress, docroot `wp`, PHP 8.3) +- **`.ddev/docker-compose.plugin.yaml`** – Mounts the repo as `wp/wp-content/plugins/batcache` + +## Troubleshooting + +**DDEV not installed:** Install [DDEV](https://ddev.com/docs/install/) (Docker is required). + +**`ddev start` fails (e.g. docroot missing):** Ensure `wp/` exists: `mkdir -p wp`. If WordPress is not in `wp/` yet, run the `wp core download` and `wp config create` / `wp core install` steps above. + +**Batcache not serving cached pages:** Ensure `WP_CACHE` is true, `advanced-cache.php` is in `wp-content/`, and an object cache (e.g. Redis with Redis Object Cache) is installed and enabled. diff --git a/advanced-cache.php b/advanced-cache.php index 6cb1029..43eaa67 100644 --- a/advanced-cache.php +++ b/advanced-cache.php @@ -1,9 +1,18 @@ group ) ) { + return false; + } + + if ( ! function_exists( 'wp_cache_flush_group' ) || ! wp_cache_supports( 'flush_group' ) ) { + return false; + } + + return wp_cache_flush_group( $batcache->group ); +} + +/** + * Adds a "Purge Batcache" node to the admin bar. + * + * @since 1.5 + * + * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance. + * @return void + */ +function batcache_admin_bar_menu( $wp_admin_bar ) { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + if ( ! function_exists( 'wp_cache_flush_group' ) || ! wp_cache_supports( 'flush_group' ) ) { + return; + } + + $purge_url = add_query_arg( + array( + 'batcache_purge' => '1', + '_wpnonce' => wp_create_nonce( 'batcache_purge_all' ), + ), + is_admin() ? admin_url() : home_url( '/' ) + ); + + $wp_admin_bar->add_node( + array( + 'id' => 'batcache-purge', + 'title' => __( 'Purge Batcache', 'batcache' ), + 'href' => $purge_url, + 'meta' => array( + 'title' => __( 'Purge entire page cache', 'batcache' ), + ), + ) + ); +} + +/** + * Handles the purge request from the admin bar link (nonce verification and redirect). + * + * Runs on init so the link works from both front-end and admin. + * + * @since 1.5 + * @return void + */ +function batcache_handle_purge_request() { + if ( ! isset( $_GET['batcache_purge'] ) || ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) { + return; + } + + $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; + if ( ! wp_verify_nonce( $nonce, 'batcache_purge_all' ) ) { + return; + } + + $flushed = batcache_flush_all(); + + // Always redirect to admin so the success/error notice can be shown. + $redirect = add_query_arg( 'batcache_purged', $flushed ? '1' : '0', admin_url() ); + + wp_safe_redirect( $redirect ); + exit; +} + +/** + * Displays an admin notice after a purge action. + * + * @since 1.5 + * @return void + */ +function batcache_purge_admin_notice() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only redirect param we set after purge. + if ( ! isset( $_GET['batcache_purged'] ) || ! current_user_can( 'manage_options' ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Value is our own redirect param (0 or 1). + $success = '1' === sanitize_text_field( wp_unslash( $_GET['batcache_purged'] ) ); + + if ( $success ) { + $message = __( 'Batcache has been purged successfully.', 'batcache' ); + $type = 'success'; + } else { + $message = __( 'Batcache purge failed or is not supported by the object cache.', 'batcache' ); + $type = 'error'; + } + + printf( + '

%2$s

', + esc_attr( $type ), + esc_html( $message ) + ); +} diff --git a/readme.txt b/readme.txt index 76c7c5f..85d0a60 100644 --- a/readme.txt +++ b/readme.txt @@ -80,6 +80,11 @@ Configuration: `.phpcs.xml.dist`, `phpstan.neon`, `phpstan-baseline.neon`, `.edi = 1.6 = +* Development: add DDEV for local WordPress environment; add `.ddev/config.yaml`, `.ddev/docker-compose.plugin.yaml` (plugin mount at `wp/wp-content/plugins/batcache`) +* Docs: add TESTING.md with DDEV one-time setup, manual testing steps, Redis/Memcached section; document Redis Object Cache installation via WP-CLI and wp-config (`WP_REDIS_HOST`, `WP_CACHE`) +* Fix: in `batcache.php`, guard now checks `isset( $wp_object_cache )` and `is_object( $wp_object_cache )` before `method_exists()` to avoid PHP 8 TypeError when object cache is not available (e.g. CLI, headless) +* Development: no automated tests (manual testing only) +* Admin bar: add "Purge Batcache" button for users with `manage_options`; purges entire batcache group via `wp_cache_flush_group()` when the object cache supports it (e.g. Redis) * Document compatibility with any WordPress object cache API backend (Redis, Memcached, etc.), not only Memcached * Update code comments and docs to use "object cache" instead of "memcached" where generic * Readme: add Object cache backend section; cite Memcached as reference setup and original dependency