array(), 'instances' => array(), ); } return $raw; } /** * Get the draft data for a post. Falls back to live data if no draft exists yet. * * @param int $post_id * @return array */ public function get_draft_data( $post_id ): array { $raw = get_post_meta( $post_id, self::DRAFT_META_KEY, true ); if ( ! is_array( $raw ) ) { return $this->get_page_data( $post_id ); } return $raw; } /** * Save the current editor state to the draft meta (does not affect the live page). * * @param int $post_id * @param array $layout * @param array $instances * @return bool */ public function save_draft( $post_id, $layout, $instances ): bool { $data = array( 'layout' => $layout, 'instances' => $instances ); update_post_meta( $post_id, self::DRAFT_META_KEY, $data ); return true; } /** * Promote the current draft to live by copying draft meta to page meta. * Stamps modified_at so drift detection can compare against dev snapshots. * * @param int $post_id * @return bool False if there is no draft to publish. */ public function publish( $post_id ): bool { $draft = get_post_meta( $post_id, self::DRAFT_META_KEY, true ); if ( ! is_array( $draft ) ) { return false; } $draft['meta']['modified_at'] = time(); update_post_meta( $post_id, self::PAGE_META_KEY, $draft ); return true; } /** * Draft-aware layout read. * * @param int $post_id * @return array */ public function get_draft_layout( $post_id ): array { $data = $this->get_draft_data( $post_id ); return isset( $data['layout'] ) ? $data['layout'] : array(); } /** * Draft-aware instance values read. * * @param int $post_id * @param string $instance_id * @return array */ public function get_draft_instance_values( $post_id, $instance_id ): array { $data = $this->get_draft_data( $post_id ); if ( ! isset( $data['instances'][ $instance_id ]['settings'] ) ) { return array(); } return $data['instances'][ $instance_id ]['settings']; } /** * Draft-aware instance blocks read. * * @param int $post_id * @param string $instance_id * @return array */ public function get_draft_instance_blocks( $post_id, $instance_id ): array { $data = $this->get_draft_data( $post_id ); if ( ! isset( $data['instances'][ $instance_id ]['blocks'] ) ) { return array(); } return $data['instances'][ $instance_id ]['blocks']; } /** * Save the full page data blob for a post. * * @param int $post_id * @param array $data Must have 'layout' and 'instances' keys. * @return bool */ public function set_page_data( $post_id, $data ) { // update_post_meta returns false when the value is unchanged — that is not an error. update_post_meta( $post_id, self::PAGE_META_KEY, $data ); return true; } /** * Get the ordered layout array for a post. * Each item: [ 'instance' => 'hero_x1', 'type' => 'hero' ] * * @param int $post_id * @return array */ public function get_layout( $post_id ) { $data = $this->get_page_data( $post_id ); return isset( $data['layout'] ) ? $data['layout'] : array(); } /** * Get field values for a specific section instance. * * @param int $post_id * @param string $instance_id * @return array Field values, keyed by field id. Empty array if not found. */ public function get_instance_values( $post_id, $instance_id ) { $data = $this->get_page_data( $post_id ); if ( ! isset( $data['instances'][ $instance_id ]['settings'] ) ) { return array(); } return $data['instances'][ $instance_id ]['settings']; } /** * Get block instances for a specific section instance. * * @param int $post_id * @param string $instance_id * @return array Ordered block instances array. */ public function get_instance_blocks( $post_id, $instance_id ) { $data = $this->get_page_data( $post_id ); if ( ! isset( $data['instances'][ $instance_id ]['blocks'] ) ) { return array(); } return $data['instances'][ $instance_id ]['blocks']; } /** * Save the full layout + all instance data for a post atomically. * * @param int $post_id * @param array $layout Ordered array of [ 'instance' => id, 'type' => slug ] * @param array $instances Keyed by instance_id: [ 'settings' => [], 'blocks' => [] ] * @return bool */ public function save_page( $post_id, $layout, $instances ) { $data = array( 'layout' => $layout, 'instances' => $instances, ); return $this->set_page_data( $post_id, $data ); } /** * Read a global region (header, footer, etc.) from wp_options. * * @param string $region Region name e.g. 'header', 'footer'. * @return array Same structure as page data (layout + instances). */ public function get_global( $region ) { $raw = get_option( self::GLOBAL_OPTION_PREFIX . $region, null ); if ( ! is_array( $raw ) ) { return array( 'layout' => array(), 'instances' => array(), ); } return $raw; } /** * Save a global region to wp_options. * * @param string $region * @param array $layout * @param array $instances * @return bool */ public function save_global( $region, $layout, $instances ) { $data = array( 'layout' => $layout, 'instances' => $instances, ); return update_option( self::GLOBAL_OPTION_PREFIX . $region, $data, false ); } /** * Return slugs of all saved global regions (e.g. ['header','footer','announcement']). * * @return string[] */ public function get_all_global_region_keys(): array { global $wpdb; $prefix = self::GLOBAL_OPTION_PREFIX; $cache_key = 'slbuild_global_region_keys'; $rows = wp_cache_get( $cache_key, 'slb-section-layout-builder' ); if ( false === $rows ) { $like = $wpdb->esc_like( $prefix ) . '%'; $rows = $wpdb->get_col( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $like ) ); wp_cache_set( $cache_key, $rows, 'slb-section-layout-builder', 300 ); } $slugs = array(); foreach ( $rows as $row ) { $slug = substr( $row, strlen( $prefix ) ); if ( $slug !== '' ) { $slugs[] = $slug; } } return $slugs; } /** * Get per-page display settings (header/footer visibility). * Defaults: both 'default' (theme-controlled). * * @param int $post_id * @return array { header: 'default'|'hidden', footer: 'default'|'hidden' } */ public function get_page_settings( int $post_id ): array { $raw = get_post_meta( $post_id, self::SETTINGS_META_KEY, true ); $defaults = array( 'header' => 'default', 'footer' => 'default' ); if ( ! is_array( $raw ) ) { return $defaults; } return array_merge( $defaults, $raw ); } /** * Save per-page display settings. * * @param int $post_id * @param array $settings * @return bool */ public function save_page_settings( int $post_id, array $settings ): bool { $allowed = array( 'default', 'hidden' ); $clean = array( 'header' => in_array( $settings['header'] ?? '', $allowed, true ) ? $settings['header'] : 'default', 'footer' => in_array( $settings['footer'] ?? '', $allowed, true ) ? $settings['footer'] : 'default', ); update_post_meta( $post_id, self::SETTINGS_META_KEY, $clean ); return true; } }