feat(cache): WP Super Cache über den Kompatibilitätskern anbinden
SuperCacheFeature, SuperCacheCompatibility (LockOnFailure, getestet 3.1.5, Version aus dem Plugin-Header) und SuperCacheIntegration ohne Tools. Der SuperCacheListener sammelt Änderungen über lit_mcp_changed und leert einmal am Request-Ende: einen Beitrag per wp_cache_post_edit, mehrere Beiträge oder seitenweite Änderungen per wp_cache_clear_cache (Multisite: eigener Blog). Komplett-Purges höchstens alle 30 s, mit genau einem Nachlauf per WP-Cron. Freigabe-Einträge leeren nichts. Uninstaller räumt Optionen, Transient und Cron-Hook ab; Reiter und lit-site-status zeigen das Profil automatisch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c383b99e7d
commit
05bf890481
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\SuperCache;
|
||||
|
||||
use LIT\MCP\Compatibility\Call;
|
||||
use LIT\MCP\Compatibility\ConstantRequirement;
|
||||
use LIT\MCP\Compatibility\GlobalRequirement;
|
||||
use LIT\MCP\Compatibility\Manifest;
|
||||
use LIT\MCP\Compatibility\Profile;
|
||||
use LIT\MCP\Compatibility\Tier;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* WP Super Cache as a compatibility profile: the two purge functions SuperCacheListener calls, checked against
|
||||
* 3.1.5 (wp-cache-phase2.php). wp_cache_post_edit is no documented API: if a release drops it, only the purge is
|
||||
* locked (visible in the tab and in lit-site-status). Both functions read the global $cache_path, which wp-cache.php
|
||||
* sets on load from wp-cache-config.php or its sample; wp_cache_post_edit also reads $blog_cache_dir
|
||||
* (wp-cache-base.php).
|
||||
*/
|
||||
final class SuperCacheCompatibility
|
||||
{
|
||||
public const LABEL = 'WP Super Cache';
|
||||
|
||||
public static function profile(): Profile
|
||||
{
|
||||
$all = SuperCacheFeature::cases();
|
||||
|
||||
return new Profile(
|
||||
id: 'wpsc',
|
||||
label: self::LABEL,
|
||||
tier: Tier::LockOnFailure,
|
||||
capabilities: $all,
|
||||
manifests: [new Manifest(
|
||||
'WP Super Cache',
|
||||
calls: [
|
||||
new Call('wp_cache_post_edit', 1, [SuperCacheFeature::PurgePost]),
|
||||
new Call('wp_cache_clear_cache', 1, [SuperCacheFeature::PurgeAll]),
|
||||
],
|
||||
constants: [new ConstantRequirement('WPCACHEHOME', $all)],
|
||||
globals: [
|
||||
new GlobalRequirement('cache_path', $all),
|
||||
new GlobalRequirement('blog_cache_dir', [SuperCacheFeature::PurgePost]),
|
||||
]
|
||||
)],
|
||||
active: static fn (): bool => defined('WPCACHEHOME'),
|
||||
version: static function (): ?string {
|
||||
if (! defined('WPCACHEHOME')) {
|
||||
return null;
|
||||
}
|
||||
// WPSC_VERSION_ID is a build counter, not the plugin version: read the plugin header.
|
||||
$version = get_file_data(WPCACHEHOME . 'wp-cache.php', ['Version' => 'Version'])['Version'] ?? '';
|
||||
|
||||
return $version === '' ? null : $version;
|
||||
},
|
||||
testedVersions: ['3.1.5']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\SuperCache;
|
||||
|
||||
use LIT\MCP\Compatibility\Capability;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// The installed PHPCompatibility sniffs predate enums (PHP 8.1) and read their methods as plain functions.
|
||||
// phpcs:disable PHPCompatibility.Variables.ForbiddenThisUseContexts.OutsideObjectContext
|
||||
|
||||
/** What the plugin does to WP Super Cache: empty its cache for one post or entirely. */
|
||||
enum SuperCacheFeature: string implements Capability
|
||||
{
|
||||
case PurgePost = 'wpsc-purge-post';
|
||||
case PurgeAll = 'wpsc-purge-all';
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PurgePost => 'Cache eines Beitrags leeren',
|
||||
self::PurgeAll => 'Gesamten Cache leeren',
|
||||
};
|
||||
}
|
||||
|
||||
public function isWrite(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\SuperCache;
|
||||
|
||||
use LIT\MCP\Compatibility\ProvidesCompatibility;
|
||||
use LIT\MCP\Integrations\Integration;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* WP Super Cache has no tools: the integration only brings the compatibility profile (SuperCacheListener does the
|
||||
* work, wired by Plugin). Without abilities it is no tool group.
|
||||
*/
|
||||
final class SuperCacheIntegration implements Integration, ProvidesCompatibility
|
||||
{
|
||||
public function id(): string
|
||||
{
|
||||
return 'wpsc';
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return SuperCacheCompatibility::LABEL;
|
||||
}
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return defined('WPCACHEHOME');
|
||||
}
|
||||
|
||||
public function abilities(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function compatibilities(): array
|
||||
{
|
||||
return [SuperCacheCompatibility::profile()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\SuperCache;
|
||||
|
||||
use Closure;
|
||||
use LIT\MCP\Admin\CompatibilityTab;
|
||||
use LIT\MCP\ChangeLog\ChangeLog;
|
||||
use LIT\MCP\Compatibility\Compatibility;
|
||||
use LIT\MCP\Log;
|
||||
use Throwable;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Empties the WP Super Cache after a write of this plugin (hook lit_mcp_changed). WPSC purges on its own only what
|
||||
* goes through wp_update_post: field, term and featured-image updates, options, menu locations, Divi design,
|
||||
* theme builder and global library modules it never sees.
|
||||
*
|
||||
* Purge with trailing run: the listener only collects, one purge runs at the end of the request (shutdown). A post
|
||||
* of a public post type (not attachment) that still exists: that post (wp_cache_post_edit, which skips drafts and
|
||||
* honours the user's "clear all on edit" setting). More than one post, or anything else: the whole cache of this
|
||||
* site, once. The whole-cache purge runs at most every THROTTLE seconds (a client looping over media updates must
|
||||
* not keep the cache empty): a purge inside the lock time schedules exactly one trailing purge (TRAILING_HOOK) at
|
||||
* its end. Single-post purges are never throttled. The release/withdraw entries of the check workflow change
|
||||
* nothing on the site and purge nothing.
|
||||
*/
|
||||
final class SuperCacheListener
|
||||
{
|
||||
public const TRAILING_HOOK = 'lit_mcp_wpsc_trailing_purge';
|
||||
/** Transient with the time of the last whole-cache purge (also in Uninstaller). */
|
||||
public const PURGED_AT = 'lit_mcp_wpsc_purged_at';
|
||||
/** Seconds between two whole-cache purges. */
|
||||
private const THROTTLE = 30;
|
||||
|
||||
private Closure $usable;
|
||||
private Closure $cacheEnabled;
|
||||
private Closure $purgePost;
|
||||
private Closure $purgeAll;
|
||||
private Closure $postIdFor;
|
||||
/** @var array<int, true> posts changed in this request */
|
||||
private array $posts = [];
|
||||
private bool $everything = false;
|
||||
private bool $queued = false;
|
||||
private bool $warned = false;
|
||||
|
||||
/**
|
||||
* Defaults are the real WPSC and WordPress functions; closures only for tests.
|
||||
*
|
||||
* @param Closure(SuperCacheFeature): bool $usable Compatibility::usable().
|
||||
* @param (Closure(): bool)|null $cacheEnabled WPSC caching switched on ($cache_enabled).
|
||||
* @param (Closure(int): void)|null $purgePost wp_cache_post_edit($postId).
|
||||
* @param (Closure(int): void)|null $purgeAll wp_cache_clear_cache($blogId), 0 = all sites.
|
||||
* @param (Closure(string, string): ?int)|null $postIdFor Post ID if objectType/objectId name an existing
|
||||
* post of a public type but attachment.
|
||||
*/
|
||||
public function __construct(
|
||||
Closure $usable,
|
||||
?Closure $cacheEnabled = null,
|
||||
?Closure $purgePost = null,
|
||||
?Closure $purgeAll = null,
|
||||
?Closure $postIdFor = null
|
||||
) {
|
||||
$this->usable = $usable;
|
||||
$this->cacheEnabled = $cacheEnabled ?? static fn (): bool => ! empty($GLOBALS['cache_enabled']);
|
||||
$this->purgePost = $purgePost ?? static function (int $id): void {
|
||||
wp_cache_post_edit($id);
|
||||
};
|
||||
$this->purgeAll = $purgeAll ?? static function (int $blog): void {
|
||||
wp_cache_clear_cache($blog);
|
||||
};
|
||||
$this->postIdFor = $postIdFor ?? self::publicPostId(...);
|
||||
}
|
||||
|
||||
public static function create(Compatibility $compatibility): self
|
||||
{
|
||||
return new self($compatibility->usable(...));
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_action(ChangeLog::CHANGED_ACTION, [$this, 'onChange'], 10, 4);
|
||||
add_action(self::TRAILING_HOOK, [$this, 'trailingPurge']);
|
||||
}
|
||||
|
||||
/** lit_mcp_changed: only collects, see flush(). Never throws. */
|
||||
public function onChange(string $ability, string $objectType, string $objectId, ?int $logId = null): void
|
||||
{
|
||||
try {
|
||||
if (CompatibilityTab::isReleaseAbility($ability) || ! ($this->cacheEnabled)()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$postId = ($this->postIdFor)($objectType, $objectId);
|
||||
if ($postId === null) {
|
||||
$this->everything = true;
|
||||
} else {
|
||||
$this->posts[$postId] = true;
|
||||
}
|
||||
if (! $this->queued) {
|
||||
$this->queued = true;
|
||||
add_action('shutdown', [$this, 'flush']);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('WP Super Cache purge', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/** shutdown: the one purge of the request. Never throws: a failed purge must not break the response. */
|
||||
public function flush(): void
|
||||
{
|
||||
$posts = array_keys($this->posts);
|
||||
$everything = $this->everything;
|
||||
$this->posts = [];
|
||||
$this->everything = false;
|
||||
|
||||
try {
|
||||
if ($everything || count($posts) > 1) {
|
||||
$this->purgeEverything();
|
||||
} elseif ($posts !== []) {
|
||||
$this->purge(SuperCacheFeature::PurgePost, fn () => ($this->purgePost)($posts[0]));
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('WP Super Cache purge', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Cron TRAILING_HOOK: the whole-cache purge a throttled one postponed. Same checks as the first try. */
|
||||
public function trailingPurge(): void
|
||||
{
|
||||
try {
|
||||
if (($this->cacheEnabled)()) {
|
||||
$this->purgeEverything();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
Log::failure('WP Super Cache trailing purge', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whole cache, at most every THROTTLE seconds. Inside the lock time one trailing purge is scheduled (cron events
|
||||
* are per site: the callback runs on this blog, no blog switch, wp_cache_clear_cache takes the blog ID).
|
||||
*/
|
||||
private function purgeEverything(): void
|
||||
{
|
||||
$this->purge(SuperCacheFeature::PurgeAll, function (): void {
|
||||
$last = get_transient(self::PURGED_AT);
|
||||
// Transients in the options table come back as strings.
|
||||
if (is_numeric($last) && time() - (int) $last < self::THROTTLE) {
|
||||
if (wp_next_scheduled(self::TRAILING_HOOK) === false) {
|
||||
wp_schedule_single_event((int) $last + self::THROTTLE, self::TRAILING_HOOK);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Known and unavoidable on a multisite: WPSC also empties the shared blogs/ meta directory of all sites
|
||||
// in its blog branch (wp-cache-phase2.php, wp_cache_clear_cache).
|
||||
($this->purgeAll)(is_multisite() ? get_current_blog_id() : 0);
|
||||
set_transient(self::PURGED_AT, time(), self::THROTTLE);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param Closure(): void $purge */
|
||||
private function purge(SuperCacheFeature $feature, Closure $purge): void
|
||||
{
|
||||
if (! ($this->usable)($feature)) {
|
||||
if (! $this->warned) {
|
||||
$this->warned = true;
|
||||
Log::line(sprintf('WP Super Cache not purged: %s not usable (see tab Compatibility)', $feature->id()));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$purge();
|
||||
}
|
||||
|
||||
private static function publicPostId(string $objectType, string $objectId): ?int
|
||||
{
|
||||
if ($objectType === 'attachment' || ! ctype_digit($objectId)) {
|
||||
return null;
|
||||
}
|
||||
$type = get_post_type_object($objectType);
|
||||
if ($type === null || empty($type->public)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_post((int) $objectId) instanceof \WP_Post ? (int) $objectId : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user