>> et <<>>. */ if ( ! defined( 'ABSPATH' ) ) { exit; } if ( class_exists( 'Aiforya_Schema_Markup_Cron', false ) ) { return; } class Aiforya_Schema_Markup_Cron { /** @var self|null Singleton instance (pattern AIFORYA canonical). */ private static ?self $instance = null; /** * Singleton accessor — convention AIFORYA canonical. * Tous les autres services et tabs accèdent à cette classe via X::get_instance(). */ public static function get_instance(): self { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } public function __construct() { $this->register_hooks(); } /** * Bind les events cron à leurs handlers. Sans ça les events planifiés ne se déclenchent pas. */ public function register_hooks(): void { add_action( 'aiforya_sm_process_new_posts', array( $this, 'run_process_new_posts' ) ); add_action( 'aiforya_sm_weekly_validation_refresh', array( $this, 'run_weekly_validation_refresh' ) ); } /** * Programme les tâches cron lors de l'activation du plugin. */ public function activate( ): void { // Planifier la tâche de traitement des nouveaux posts (toutes les heures) if ( ! wp_next_scheduled( 'aiforya_sm_process_new_posts' ) ) { wp_schedule_event( time(), 'hourly', 'aiforya_sm_process_new_posts' ); } // Planifier la tâche de revalidation hebdomadaire if ( ! wp_next_scheduled( 'aiforya_sm_weekly_validation_refresh' ) ) { wp_schedule_event( time(), 'weekly', 'aiforya_sm_weekly_validation_refresh' ); } } /** * Nettoie les tâches cron lors de la désactivation. */ public function deactivate( ): void { // Nettoyer la tâche de traitement des nouveaux posts $timestamp = wp_next_scheduled( 'aiforya_sm_process_new_posts' ); if ( $timestamp ) { wp_unschedule_event( $timestamp, 'aiforya_sm_process_new_posts' ); } wp_clear_scheduled_hook( 'aiforya_sm_process_new_posts' ); // Nettoyer la tâche de revalidation hebdomadaire $timestamp = wp_next_scheduled( 'aiforya_sm_weekly_validation_refresh' ); if ( $timestamp ) { wp_unschedule_event( $timestamp, 'aiforya_sm_weekly_validation_refresh' ); } wp_clear_scheduled_hook( 'aiforya_sm_weekly_validation_refresh' ); } /** * Tâche cron qui traite les nouveaux posts n'ayant pas encore de schéma. */ public function run_process_new_posts( ): void { // Récupérer le nombre de posts à traiter par batch depuis les options $batch_size = (int) get_option( 'aiforya_sm_cron_batch_size', 10 ); if ( $batch_size <= 0 ) { $batch_size = 10; } // Obtenir les posts sans schéma via le service Db $db = Aiforya_Schema_Markup_Db::get_instance(); $posts_without_schema = $db->get_posts_without_schema( $batch_size ); if ( empty( $posts_without_schema ) ) { return; } // Obtenir le service Generator pour générer les schémas $generator = Aiforya_Schema_Markup_Generator::get_instance(); // Traiter chaque post — extraire l'ID proprement (Db retourne des stdClass). $stats = array( 'ok' => 0, 'fail' => 0, 'skipped' => 0 ); foreach ( $posts_without_schema as $row ) { $post_id = is_object( $row ) ? (int) ( $row->ID ?? 0 ) : (int) $row; if ( ! $post_id ) { $stats['skipped']++; continue; } $post = get_post( $post_id ); if ( ! $post || $post->post_status !== 'publish' ) { $stats['skipped']++; continue; } $result = $generator->generate_and_save_schema_for_post( $post_id, false ); if ( is_wp_error( $result ) ) { $stats['fail']++; // Journal de diagnostic retiré de la variante wordpress.org. } else { $stats['ok']++; } } update_option( 'aiforya_sm_last_cron_run', array( 'time' => time(), 'ok' => $stats['ok'], 'fail' => $stats['fail'], 'skipped' => $stats['skipped'], ), false ); } /** * Génère/régénère le schéma pour UN post précis (utilisé par AJAX bouton "Générer ce post"). */ public function generate_for_one_post( int $post_id, bool $force = true ) { $post = get_post( $post_id ); if ( ! $post || $post->post_status !== 'publish' ) { return new WP_Error( 'invalid_post', __( 'Invalid or unpublished post.', 'aiforya-schema-markup' ) ); } $generator = Aiforya_Schema_Markup_Generator::get_instance(); return $generator->generate_and_save_schema_for_post( $post_id, $force ); } /** * Génère en bulk pour TOUS les posts publiés sans schéma (limite par batch pour éviter timeout). * Retourne stats { ok, fail, skipped, remaining }. */ public function generate_bulk( int $batch_size = 25 ): array { $db = Aiforya_Schema_Markup_Db::get_instance(); $posts_without_schema = $db->get_posts_without_schema( $batch_size ); if ( empty( $posts_without_schema ) ) { return array( 'ok' => 0, 'fail' => 0, 'skipped' => 0, 'remaining' => 0, 'errors' => array() ); } $generator = Aiforya_Schema_Markup_Generator::get_instance(); $stats = array( 'ok' => 0, 'fail' => 0, 'skipped' => 0 ); $errors = array(); // Collecte les 5 premières erreurs détaillées pour diagnostic. foreach ( $posts_without_schema as $row ) { $post_id = is_object( $row ) ? (int) ( $row->ID ?? 0 ) : (int) $row; if ( ! $post_id ) { $stats['skipped']++; continue; } $post = get_post( $post_id ); if ( ! $post || $post->post_status !== 'publish' ) { $stats['skipped']++; continue; } $result = $generator->generate_and_save_schema_for_post( $post_id, false ); if ( is_wp_error( $result ) ) { $stats['fail']++; if ( count( $errors ) < 5 ) { $errors[] = sprintf( '#%d "%s" : %s [%s]', $post_id, wp_trim_words( $post->post_title, 6 ), $result->get_error_message(), $result->get_error_code() ); } } else { $stats['ok']++; } } // Estimation des restants : count posts sans schéma après traitement. $remaining = $db->get_posts_without_schema( 9999 ); $stats['remaining'] = is_array( $remaining ) ? count( $remaining ) : 0; $stats['errors'] = $errors; return $stats; } /** * Tâche cron hebdomadaire pour revalider les schémas existants. */ public function run_weekly_validation_refresh( ): void { // Récupérer le nombre de schémas à revalider par batch depuis les options $batch_size = (int) get_option( 'aiforya_sm_validation_batch_size', 50 ); if ( $batch_size <= 0 ) { $batch_size = 50; } // Obtenir les schémas à revalider via le service Db $db = Aiforya_Schema_Markup_Db::get_instance(); $schemas_to_revalidate = $db->get_schemas_to_revalidate( $batch_size ); if ( empty( $schemas_to_revalidate ) ) { return; } // Obtenir le service Validator pour revalider // Note: Le contract ne mentionne pas de service Validator, donc on va simuler une validation basique // En production, il faudrait appeler l'API Google Rich Results Test foreach ( $schemas_to_revalidate as $schema ) { if ( ! isset( $schema->id ) || ! isset( $schema->post_id ) ) { continue; } // Vérifier que le post existe encore et est publié $post = get_post( $schema->post_id ); if ( ! $post || $post->post_status !== 'publish' ) { continue; } // Revalider le schéma (validation basique : vérifier que c'est un JSON valide) $is_valid = false; $validation_message = ''; if ( ! empty( $schema->schema_json ) ) { $decoded = json_decode( $schema->schema_json, true ); if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) { $is_valid = true; $validation_message = 'Valid JSON schema'; } else { $validation_message = 'Invalid JSON schema: ' . json_last_error_msg(); } } else { $validation_message = 'Empty JSON schema'; } // Logger la validation $db->log_validation( array( 'schema_id' => $schema->id, 'post_id' => $schema->post_id, 'status' => $is_valid ? 'valid' : 'invalid', 'response' => wp_json_encode( array( 'message' => $validation_message, 'source' => 'weekly_cron', 'date' => current_time( 'mysql' ), ) ), 'validated_at' => current_time( 'mysql' ), ) ); } } }