*/ private $layer_source; private WordPressPathProvider $paths; /** * @param callable():array|null $layer_source Testable local cache adapter. */ public function __construct( FindingFactory $findings, ?callable $layer_source = null, ?WordPressPathProvider $paths = null ) { $this->findings = $findings; $this->layer_source = $layer_source ?? array( $this, 'wordpress_layers' ); $this->paths = $paths ?? new WordPressPathProvider(); } public function id(): string { return 'cache.layers'; } /** * @return array */ public function execute( string $timestamp ): array { try { $layers = call_user_func( $this->layer_source ); } catch ( Throwable $exception ) { return $this->error_finding( $timestamp ); } if ( ! is_array( $layers ) ) { return $this->error_finding( $timestamp ); } $observations = array(); foreach ( self::FIELDS as $field ) { if ( isset( $layers[ $field ] ) && is_bool( $layers[ $field ] ) ) { $observations[] = array( 'layer' => $field, 'enabled' => $layers[ $field ], ); } } $observed = count( $observations ); $total = count( self::FIELDS ); $api_available = isset( $layers['cache_api_available'] ) && true === $layers['cache_api_available']; $object_conflict = isset( $layers['object_cache_dropin'], $layers['external_object_cache'] ) && true === $layers['object_cache_dropin'] && false === $layers['external_object_cache']; $page_conflict = isset( $layers['advanced_cache_dropin'], $layers['wp_cache_enabled'] ) && true === $layers['advanced_cache_dropin'] && false === $layers['wp_cache_enabled']; if ( 0 === $observed ) { $status = 'not_tested'; $coverage = 'none'; } elseif ( isset( $layers['cache_api_available'] ) && ! $api_available ) { $status = 'fail'; $coverage = $observed === $total ? 'complete' : 'partial'; } elseif ( $object_conflict || $page_conflict ) { $status = 'warning'; $coverage = $observed === $total ? 'complete' : 'partial'; } elseif ( $observed < $total ) { $status = 'not_tested'; $coverage = 'partial'; } else { $status = 'pass'; $coverage = 'complete'; } return $this->findings->build( $this->id(), $status, array( 'observations' => $observations, 'method' => 'wordpress_cache_presence_signals', 'limitations' => array( 'Cache operations, callbacks, keys, values, hit rates and configuration contents were not inspected. An absent optional cache layer is not treated as a defect.' ), ), $observed, $total, $coverage, $timestamp ); } /** * @return array */ private function wordpress_layers(): array { return array( 'cache_api_available' => function_exists( 'wp_cache_get' ) && function_exists( 'wp_cache_set' ) && function_exists( 'wp_cache_delete' ), 'external_object_cache' => function_exists( 'wp_using_ext_object_cache' ) && wp_using_ext_object_cache(), 'object_cache_dropin' => file_exists( $this->paths->content_file( 'object-cache.php' ) ), 'wp_cache_enabled' => defined( 'WP_CACHE' ) && (bool) WP_CACHE, 'advanced_cache_dropin' => file_exists( $this->paths->content_file( 'advanced-cache.php' ) ), 'opcache_available' => extension_loaded( 'Zend OPcache' ), ); } /** * @return array */ private function error_finding( string $timestamp ): array { return $this->findings->build( $this->id(), 'error', array( 'observations' => array(), 'method' => 'wordpress_cache_presence_signals', 'limitations' => array( 'The local cache adapter failed; no exception, path, key, value or configuration content was stored.' ), ), 0, count( self::FIELDS ), 'error', $timestamp ); } }