Request cache for group instances */ private static array $request_cache = []; private ?array $groups_for_select = null; /** * Gets a group instance by its ID. * Uses a request cache and triggers backend processing. * * @param int $id The id of the group to retrieve. * * @return Group|null The processed Group object or null if not found. */ public function get( int $id, bool $process_data = true ): ?Group { if ( array_key_exists( $id, self::$request_cache ) ) { return self::$request_cache[ $id ]; } $group = GroupFactory::make( $id ); if ( $group instanceof Group ) { if ( $process_data ) { $group->processBackendData(); self::$request_cache[ $id ] = $group; } return $group; } else { if ( $process_data ) { self::$request_cache[ $id ] = null; } return null; } } /** * Gets all groups. * * @param int[]|null $ids_override Fetch exactly these IDs instead of querying by $post_status * * @return Group[] Array of Group objects, processed and cached. */ public function get_all( $post_status = [ 'publish', 'draft' ], bool $process_data = true, bool $only_ids = false, ?array $ids_override = null ): array { if ( $process_data && $only_ids ) { $process_data = false; } $posts = $ids_override ?? get_posts( [ 'post_type' => Constants::POST_TYPE_GROUP, 'post_status' => $post_status, 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'cache_results' => false, ] ); if ( empty( $posts ) ) { return []; } if ( $only_ids ) { return $posts; } // Two IN() queries for all post + meta rows. _prime_post_caches( $posts, false, true ); $groups = []; foreach ( $posts as $post_id ) { $groups[] = $this->get( $post_id, $process_data ); } return array_filter( $groups ); } /** * Prepares the data for Groups for the select field in the block editor and for the placement item select. */ public function get_groups_for_select(): array { if ( null !== $this->groups_for_select ) { return $this->groups_for_select; } $groups = $this->get_all( [ 'publish', 'draft' ], false ); $options = []; foreach ( $groups as $group ) { if ( $group instanceof Group ) { /* translators: %d is the group id. */ $options[] = [ 'value' => $group->get_id(), 'label' => $group->get_title() ?: sprintf( __( 'Group #%d', 'adpresso' ), $group->get_id() ), ]; } } $this->groups_for_select = $options; return $options; } /** * Empty the request cache. */ public static function clear_request_cache(): void { self::$request_cache = []; } /** * Saves or updates a Group entity. * This method handles core post data and delegates meta data saving * to the Metabox_Group orchestrator for consistency and reuse. * * @param Group $group The Group entity object. * @param array $data The raw, validated data from the REST request. * * @return Group|null The saved and re-fetched Group object, or null on failure. */ public function save( Group $group, array $data ): ?Group { // Prepare and sanitize core post data using the Params utility. // REST params arrive unslashed; ParamsHandler and the boxes expect slashed input like $_POST. $params = new ParamsHandler( wp_slash( $data ) ); $group_id = $group->get_id(); $post_args = [ 'post_type' => Constants::POST_TYPE_GROUP, 'post_status' => $params->getKey( 'status', 'draft' ), 'post_title' => $params->getString( 'title' ), ]; if ( is_int( $group_id ) && $group_id > 0 ) { $post_args['ID'] = $group_id; } // Insert or Update the Post data $post_id = wp_insert_post( wp_slash( $post_args ), true ); if ( is_wp_error( $post_id ) ) { RuntimeDebugger::add_server_error_log_entry( 'GroupRepository Save Error: ' . $post_id->get_error_message() ); return null; } // Delegate Meta Data Saving to the Orchestrator. Deliberately NOT sanitize_text_field() - that would blanket-strip every string value // in meta_data, including HTML/scripts in fields that are SUPPOSED to preserve them for trusted users. $meta_data = $params->getArray( 'meta_data', fn( $value ) => $value, [] ); if ( ! empty( $meta_data ) ) { // Instantiate the orchestrator for groups. $metabox_orchestrator = new Group_Metaboxes_Orchestrator(); // Call the public save methods on the orchestrators. if ( method_exists( $metabox_orchestrator, 'process_save' ) ) { $metabox_orchestrator->process_save( $post_id, wp_slash( $meta_data ) ); } } // Clear Caches and Return the fresh, fully processed object. $this->clear_request_cache_for( $post_id ); return $this->get( $post_id ); } /** * Ads an Ad to the metadata of a group. * * Wenn die Anzeige bereits in der Gruppe vorhanden ist, wird keine Aktion ausgeführt. * If the ad is already in the group, do nothing. * Else add it with the default weight of 10. * * @param int $group_id ID of the group, the ad should be added to. * @param int $ad_id ID of the ad that should be added * * @return bool True for success. */ public function add_ad_to_group( int $group_id, int $ad_id ): bool { if ( $group_id <= 0 || $ad_id <= 0 ) { return false; } $meta_key = Constants::META_PREFIX . 'ads_with_weights'; $ads_with_weights = get_post_meta( $group_id, $meta_key, true ); if ( ! is_array( $ads_with_weights ) ) { $ads_with_weights = []; } // Check for duplicates. if ( array_key_exists( $ad_id, $ads_with_weights ) ) { return true; } $ads_with_weights[ $ad_id ] = 10; $result = update_post_meta( $group_id, $meta_key, $ads_with_weights ); if ( $result ) { $this->clear_request_cache_for( $group_id ); } return $result; } /** * Entfernt eine Anzeige aus den Metadaten einer Gruppe. * * Wenn die Anzeige nicht in der Gruppe vorhanden ist, wird die Aktion * als erfolgreich betrachtet, da der gewünschte Zustand bereits erreicht ist. * * @param int $group_id Die ID der Gruppe, aus der die Anzeige entfernt werden soll. * @param int $ad_id Die ID der zu entfernenden Anzeige. * * @return bool True for success. */ public function remove_ad_from_group( int $group_id, int $ad_id ): bool { if ( $group_id <= 0 || $ad_id <= 0 ) { return false; } $meta_key = Constants::META_PREFIX . 'ads_with_weights'; $ads_with_weights = get_post_meta( $group_id, $meta_key, true ); // Check if part of the group. if ( ! is_array( $ads_with_weights ) || ! array_key_exists( $ad_id, $ads_with_weights ) ) { return true; } unset( $ads_with_weights[ $ad_id ] ); $result = update_post_meta( $group_id, $meta_key, $ads_with_weights ); if ( $result ) { $this->clear_request_cache_for( $group_id ); } return $result; } /** * Removes a Group entity from the database. * * @param Group $group The Group entity object to delete. * @param bool $force If true, deletes the post permanently. * * @return bool True on success, false on failure. */ public function delete( Group $group, bool $force = false ): bool { $post_id = $group->get_id(); if ( $post_id <= 0 ) { return false; } // wp_delete_post() only auto-routes to wp_trash_post() for the core 'post'/'page' types. $result = $force ? wp_delete_post( $post_id, true ) : wp_trash_post( $post_id ); if ( $result !== false && $result !== null ) { $this->clear_request_cache_for( $post_id ); return true; } return false; } /** * Gets all groups that contain a specific ad. * * @param int $ad_id ID of the ad to search for. * @param array $post_status Optional: Post-Statuses to filter groups by (default: ['publish', 'draft']). * @param bool $process_data * @param bool $only_ids * * @return Group[] Array of Group objects that contain the ad. */ public function find_by_contained_ad( int $ad_id, array $post_status = [ 'publish', 'draft' ], bool $process_data = true, bool $only_ids = false ): array { static $cache = []; $cache_key = md5( serialize( [ $ad_id, $post_status, $only_ids ] ) ); if ( isset( $cache[ $cache_key ] ) ) { return $cache[ $cache_key ]; } if ( $ad_id <= 0 ) { return []; } $meta_key = Constants::META_PREFIX . 'ads_with_weights'; $query_args = [ 'post_type' => Constants::POST_TYPE_GROUP, 'post_status' => $post_status, 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'cache_results' => false, 'meta_query' => [ [ 'key' => $meta_key, 'compare' => 'EXISTS', ], ], ]; $query = new WP_Query( $query_args ); $group_ids = $query->posts; if ( empty( $group_ids ) ) { return []; } // Optional: Meta-Cache für alle Gruppen primen (Performance) update_meta_cache( 'post', $group_ids ); $found_groups = []; foreach ( $group_ids as $group_id ) { $meta_value = get_post_meta( $group_id, $meta_key, true ); if ( empty( $meta_value ) ) { continue; } $ad_data = is_array( $meta_value ) ? $meta_value : maybe_unserialize( $meta_value ); if ( is_array( $ad_data ) && isset( $ad_data[ $ad_id ] ) ) { if ( $only_ids ) { $found_groups[] = $group_id; } else { $group_object = $this->get( $group_id, $process_data ); if ( $group_object instanceof Group ) { $found_groups[] = $group_object; } } } } $cache[ $cache_key ] = $found_groups; return $found_groups; } /** * Bulk version of find_by_contained_ad(). * * @param int[] $ad_ids The ad IDs to look up. * @param array $post_status Group post statuses to include. * * @return array Ad ID => the Groups that contain it. */ public function find_containing_ads_map( array $ad_ids, array $post_status = [ 'publish', 'draft' ] ): array { $ad_ids = array_values( array_unique( array_filter( $ad_ids ) ) ); if ( empty( $ad_ids ) ) { return []; } $meta_key = Constants::META_PREFIX . 'ads_with_weights'; $query_args = [ 'post_type' => Constants::POST_TYPE_GROUP, 'post_status' => $post_status, 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'cache_results' => false, 'meta_query' => [ [ 'key' => $meta_key, 'compare' => 'EXISTS', ], ], ]; $query = new WP_Query( $query_args ); $group_ids = $query->posts; if ( empty( $group_ids ) ) { return []; } update_meta_cache( 'post', $group_ids ); $wanted = array_flip( $ad_ids ); $map = []; foreach ( $group_ids as $group_id ) { $meta_value = get_post_meta( $group_id, $meta_key, true ); if ( empty( $meta_value ) ) { continue; } $ad_data = is_array( $meta_value ) ? $meta_value : maybe_unserialize( $meta_value ); if ( ! is_array( $ad_data ) ) { continue; } $group_object = null; foreach ( array_intersect_key( $ad_data, $wanted ) as $ad_id => $weight ) { if ( null === $group_object ) { $group_object = $this->get( $group_id, false ); if ( ! $group_object instanceof Group ) { break; } } $map[ $ad_id ][] = $group_object; } } return $map; } /** * Helper to clear the internal request cache for a specific ID. * * @param int $post_id */ public function clear_request_cache_for( int $post_id ): void { unset( self::$request_cache[ $post_id ] ); } }