File: /var/www/html/wp-content/plugins/wordpress-seo/src/schema-aggregator/application/cache/manager.php
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Schema_Aggregator\Application\Cache;
use Exception;
use Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Config;
/**
* Cache manager
*
* Manages cache storage/retrieval/invalidation using WordPress Transients API.
*/
class Manager {
/**
* Cache key prefix
*
* @var string
*/
private const CACHE_PREFIX = 'yoast_schema_aggregator';
/**
* Cache version for invalidation
*
* @var int
*/
private const CACHE_VERSION = 1;
/**
* Configuration provider
*
* @var Config
*/
private $config;
/**
* Constructor
*
* @param Config $config Configuration provider.
*/
public function __construct( Config $config ) {
$this->config = $config;
}
/**
* Get cached data for a page
*
* @param string $post_type The post type that the cache is for.
* @param int $page Page number.
* @param int $per_page Items per page.
*
* @return array<string>|null Cached data or null.
*/
public function get( string $post_type, int $page, int $per_page ): ?array {
try {
if ( ! $this->config->cache_enabled() ) {
return null;
}
if ( $page < 1 || $per_page < 1 ) {
return null;
}
$key = $this->get_cache_key( $post_type, $page, $per_page );
$data = \get_transient( $key );
if ( $data === false ) {
return null;
}
if ( ! \is_array( $data ) ) {
\delete_transient( $key );
return null;
}
return $data;
} catch ( Exception $e ) {
return null;
}
}
/**
* Set cache data for a page
*
* @param string $post_type The post type that the cache is for.
* @param int $page Page number.
* @param int $per_page Items per page.
* @param array<string> $data Data to cache.
*
* @return bool Success.
*/
public function set( string $post_type, int $page, int $per_page, array $data ): bool {
try {
if ( $page < 1 || $per_page < 1 || empty( $data ) ) {
return false;
}
$key = $this->get_cache_key( $post_type, $page, $per_page );
$expiration = $this->config->get_expiration( $data );
return \set_transient( $key, $data, $expiration );
} catch ( Exception $e ) {
return false;
}
}
/**
* Invalidate cache for specific page/per_page combination or all pages
*
* Note: When invalidating a specific page without per_page, this clears
* ALL per_page variations for that page using a wildcard pattern.
*
* @param string $post_type The post type that the cache is for.
* @param int|null $page Page number or null for all.
* @param int|null $per_page Items per page or null to clear all per_page variations.
*
* @return bool Success.
*/
public function invalidate( string $post_type, ?int $page = null, ?int $per_page = null ): bool {
if ( $page !== null && $per_page !== null ) {
// Clear specific page/per_page combination.
return \delete_transient( $this->get_cache_key( $post_type, $page, $per_page ) );
}
if ( $page !== null && $per_page === null ) {
// Clear all per_page variations for this page.
global $wpdb;
if ( ! isset( $wpdb ) || ! \is_object( $wpdb ) ) {
return false;
}
$pattern = '_transient_' . self::CACHE_PREFIX . '_page_' . $page . '_per_%';
$timeout_pattern = '_transient_timeout_' . self::CACHE_PREFIX . '_page_' . $page . '_per_%';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
$deleted = $wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
$pattern,
$timeout_pattern,
),
);
return $deleted !== false;
}
return $this->invalidate_all();
}
/**
* Invalidate all cache pages
*
* @return bool Success.
*/
public function invalidate_all(): bool {
try {
global $wpdb;
if ( ! isset( $wpdb ) || ! \is_object( $wpdb ) ) {
return false;
}
// Pattern matches: yoast_schema__aggregator_page_{n}_per_{m}_v{version}.
$pattern = '_transient_' . self::CACHE_PREFIX . '_page_%';
$timeout_pattern = '_transient_timeout_' . self::CACHE_PREFIX . '_page_%';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
$deleted = $wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
$pattern,
$timeout_pattern,
),
);
if ( $deleted === false ) {
return false;
}
return true;
} catch ( Exception $e ) {
return false;
}
}
/**
* Generate cache key for page.
*
* @param string $post_type The post type that the cache is for.
* @param int $page Page number.
* @param int $per_page Items per page.
*
* @return string Cache key.
*/
private function get_cache_key( string $post_type, int $page, int $per_page ): string {
return \sprintf(
'%s_page_%d_per_%d_type_%s_v%d',
self::CACHE_PREFIX,
$page,
$per_page,
$post_type,
self::CACHE_VERSION,
);
}
}