$schema The schema data to validate. * * @return array Validation result with 'valid', 'errors', 'warnings' keys. */ public function validate( int $product_id, array $schema ): array { $cache_key = self::CACHE_PREFIX . $product_id; $cached = get_transient( $cache_key ); if ( false !== $cached && is_array( $cached ) ) { return $cached; } $result = $this->run_local_validation( $schema ); set_transient( $cache_key, $result, self::CACHE_TTL ); return $result; } /** * Run local structural validation on the schema. * * Checks for required properties per schema.org specifications * without making an external API call. This provides instant feedback * and reduces API dependency. * * @param array $schema The schema data. * * @return array Validation result. */ public function run_local_validation( array $schema ): array { $errors = array(); $warnings = array(); // Check @context. if ( ! isset( $schema['@context'] ) || 'https://schema.org' !== $schema['@context'] ) { $errors[] = __( 'Missing or invalid @context — must be "https://schema.org".', 'ai-discoverability-kit' ); } // Check @type. if ( ! isset( $schema['@type'] ) ) { $errors[] = __( 'Missing @type property.', 'ai-discoverability-kit' ); } // Product-specific validation. if ( isset( $schema['@type'] ) && 'Product' === $schema['@type'] ) { $errors = array_merge( $errors, $this->validate_product( $schema ) ); $warnings = array_merge( $warnings, $this->warn_product( $schema ) ); } return array( 'valid' => empty( $errors ), 'errors' => $errors, 'warnings' => $warnings, 'validated_at' => current_time( 'mysql' ), 'schema_hash' => md5( (string) wp_json_encode( $schema ) ), ); } /** * Validate required Product schema properties. * * @param array $schema Product schema data. * * @return array Error messages. */ private function validate_product( array $schema ): array { $errors = array(); $required = array( 'name', 'image' ); foreach ( $required as $prop ) { if ( empty( $schema[ $prop ] ) ) { $errors[] = sprintf( /* translators: %s: Property name */ __( 'Required property "%s" is missing or empty.', 'ai-discoverability-kit' ), $prop ); } } // Offers validation. if ( empty( $schema['offers'] ) ) { $errors[] = __( 'Missing "offers" property — price information is required.', 'ai-discoverability-kit' ); } else { $errors = array_merge( $errors, $this->validate_offers( $schema['offers'] ) ); } return $errors; } /** * Validate Offer/AggregateOffer schema. * * @param array $offers Offers schema data. * * @return array Error messages. */ private function validate_offers( array $offers ): array { $errors = array(); $type = $offers['@type'] ?? ''; if ( 'Offer' === $type ) { $required_offer = array( 'price', 'priceCurrency', 'availability' ); foreach ( $required_offer as $prop ) { if ( empty( $offers[ $prop ] ) ) { $errors[] = sprintf( /* translators: %s: Property name */ __( 'Offer is missing required property "%s".', 'ai-discoverability-kit' ), $prop ); } } } elseif ( 'AggregateOffer' === $type ) { if ( ! isset( $offers['lowPrice'] ) || ! isset( $offers['highPrice'] ) ) { $errors[] = __( 'AggregateOffer must include "lowPrice" and "highPrice".', 'ai-discoverability-kit' ); } if ( empty( $offers['priceCurrency'] ) ) { $errors[] = __( 'AggregateOffer is missing "priceCurrency".', 'ai-discoverability-kit' ); } } return $errors; } /** * Generate warnings for recommended but not required properties. * * @param array $schema Product schema data. * * @return array Warning messages. */ private function warn_product( array $schema ): array { $warnings = array(); $recommended = array( 'description' => __( 'Adding a "description" improves AI discoverability.', 'ai-discoverability-kit' ), 'sku' => __( 'Adding a "sku" helps product identification.', 'ai-discoverability-kit' ), 'brand' => __( 'Adding a "brand" strengthens entity relationships.', 'ai-discoverability-kit' ), 'gtin' => __( 'Adding a "gtin" enables barcode-based product matching.', 'ai-discoverability-kit' ), ); foreach ( $recommended as $prop => $message ) { if ( empty( $schema[ $prop ] ) ) { $warnings[] = $message; } } // Check AggregateRating. if ( empty( $schema['aggregateRating'] ) ) { $warnings[] = __( 'Products with reviews and ratings rank higher in AI answers.', 'ai-discoverability-kit' ); } return $warnings; } /** * Clear the validation cache for a specific product. * * Called when product data is updated. * * @param int $product_id The product ID. * * @return void */ public function clear_cache( int $product_id ): void { delete_transient( self::CACHE_PREFIX . $product_id ); } /** * Clear all validation caches. * * @return void */ public function clear_all_caches(): void { global $wpdb; // Safe: internal $wpdb->prefix table names; user values use placeholders. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", '_transient_' . self::CACHE_PREFIX . '%', '_transient_timeout_' . self::CACHE_PREFIX . '%' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter } }